commit c0d23dbbe1996abbc972163ec3bb860b6d47a981 Author: amos Date: Wed May 21 14:41:59 2025 +0800 upload source code diff --git a/.asf.yaml b/.asf.yaml new file mode 100644 index 0000000..b1872d1 --- /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 0000000..6ebc08d --- /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 0000000..7cbea07 --- /dev/null +++ b/.bazelversion @@ -0,0 +1 @@ +5.2.0 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..8f1cc71 --- /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 0000000..870c2b1 --- /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 0000000..e689284 --- /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 0000000..cac503d --- /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 0000000..8361b8a --- /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 new file mode 100644 index 0000000..96bffa5 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ + + +### Which Issue(s) This PR Fixes + + + +Fixes #issue_id + +### Brief Description + + + +### How Did You Test This Change? + + diff --git a/.github/asf-deploy-settings.xml b/.github/asf-deploy-settings.xml new file mode 100644 index 0000000..246bf09 --- /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 0000000..510457c --- /dev/null +++ b/.github/workflows/bazel.yml @@ -0,0 +1,23 @@ +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 0000000..e5e8d32 --- /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 0000000..b249072 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,25 @@ +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@v4 + with: + java-version: "8" + distribution: "corretto" + 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 + token: cf0cba0a-22f8-4580-89ab-4f1dec3bda6f diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml new file mode 100644 index 0000000..ad5bcbd --- /dev/null +++ b/.github/workflows/integration-test.yml @@ -0,0 +1,45 @@ +name: Run Integration Tests +on: + pull_request: + types: [opened, reopened, synchronize] + push: + branches: [master, develop] + +jobs: + it-test: + name: "maven-compile (${{ matrix.os }}, JDK-${{ matrix.jdk }})" + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + jdk: [8] + steps: + - name: Cache Maven Repos + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.jdk }} + distribution: "corretto" + cache: "maven" + + - name: Run integration tests with Maven + run: mvn clean verify -Pit-test -Pskip-unit-tests + + - name: Publish Test Report + uses: mikepenz/action-junit-report@v3 + if: always() + with: + report_paths: 'test/target/failsafe-reports/TEST-*.xml' + annotate_only: true + include_passed: true + detailed_summary: true diff --git a/.github/workflows/license-checker.yaml b/.github/workflows/license-checker.yaml new file mode 100644 index 0000000..259fdd7 --- /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 0000000..4eacd65 --- /dev/null +++ b/.github/workflows/maven.yaml @@ -0,0 +1,46 @@ +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@v4 + with: + java-version: ${{ matrix.jdk }} + # See https://github.com/actions/setup-java?tab=readme-ov-file#supported-distributions + # AdoptOpenJDK got moved to Eclipse Temurin and won't be updated anymore. + distribution: "corretto" + cache: "maven" + - name: Build with Maven + run: mvn -B package --file pom.xml + + - name: Upload Auth JVM crash logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: jvm-crash-logs + path: /Users/runner/work/rocketmq/rocketmq/auth/hs_err_pid*.log + retention-days: 1 + + - name: Upload broker JVM crash logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: jvm-crash-logs + path: /Users/runner/work/rocketmq/rocketmq/broker/hs_err_pid*.log + retention-days: 1 diff --git a/.github/workflows/misspell_check.yml b/.github/workflows/misspell_check.yml new file mode 100644 index 0000000..81729e4 --- /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 0000000..99d7309 --- /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@v4 + 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@v4 + 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 0000000..0bc74a6 --- /dev/null +++ b/.github/workflows/pr-e2e-test.yml @@ -0,0 +1,263 @@ +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: + if: > + github.repository == 'apache/rocketmq' && + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + matrix: + base-image: ["ubuntu"] + java-version: ["8"] + steps: + - name: 'Download artifact' + uses: actions/github-script@v6 + with: + script: | + let artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{github.event.workflow_run.id }}, + }); + let matchArtifactRmq = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == "rocketmq" + })[0]; + let download = await github.rest.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@v4 + name: Upload distribution tar + with: + name: versionlist + path: rocketmq-docker/image-build-ci/versionlist/* + + list-version: + if: > + github.repository == 'apache/rocketmq' && + 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@v4 + 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@v4 + 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 + LATEST_GO_VERSION=$(curl -s https://go.dev/VERSION?m=text | awk 'NR==1') + wget "https://go.dev/dl/${LATEST_GO_VERSION}.linux-amd64.tar.gz" && \ + rm -rf /usr/local/go && tar -C /usr/local -xzf ${LATEST_GO_VERSION}.linux-amd64.tar.gz + 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@v4 + 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@v4 + 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 }} \ No newline at end of file diff --git a/.github/workflows/push-ci.yml b/.github/workflows/push-ci.yml new file mode 100644 index 0000000..6de8595 --- /dev/null +++ b/.github/workflows/push-ci.yml @@ -0,0 +1,348 @@ +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: + if: github.repository == 'apache/rocketmq' + 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@v4 + 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@v4 + 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@v4 + name: Upload distribution tar + with: + name: versionlist + path: rocketmq-docker/image-build-ci/versionlist/* + + list-version: + if: > + github.repository == 'apache/rocketmq' && + 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@v4 + 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-e2e: + if: ${{ success() }} + name: Deploy RocketMQ For E2E + 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 }} + + deploy-benchmark: + if: ${{ success() }} + name: Deploy RocketMQ For Benchmarking + 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: "001-${{ 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-e2e] + 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@v4 + 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-e2e] + 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 + LATEST_GO_VERSION=$(curl -s https://go.dev/VERSION?m=text | awk 'NR==1') + wget "https://go.dev/dl/${LATEST_GO_VERSION}.linux-amd64.tar.gz" && \ + rm -rf /usr/local/go && tar -C /usr/local -xzf ${LATEST_GO_VERSION}.linux-amd64.tar.gz + 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@v4 + 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-e2e ] + 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@v4 + if: always() + name: Upload test log + with: + name: test-e2e-remoting-java-log.txt + path: testlog.txt + + benchmark-test: + if: ${{ success() }} + runs-on: ubuntu-latest + name: Performance benchmark test + needs: [ list-version, deploy-benchmark ] + timeout-minutes: 60 + steps: + - uses: apache/rocketmq-test-tool/benchmark-runner@ce372e5f3906ca1891e4918b05be14608eae608e + name: Performance benchmark + with: + action: "performance-benchmark" + ask-config: "${{ secrets.ASK_CONFIG_VIRGINA }}" + job-id: "001-${{ strategy.job-index }}" + # The time to run the test, 15 minutes + test-time: "900" + # Some thresholds set in advance + min-send-tps-threshold: "12000" + max-rt-ms-threshold: "500" + avg-rt-ms-threshold: "10" + max-2c-rt-ms-threshold: "150" + avg-2c-rt-ms-threshold: "10" + - name: Upload test report + if: always() + uses: actions/upload-artifact@v4 + with: + name: benchmark-report + path: benchmark/ + + clean-e2e: + if: always() + name: Clean E2E + 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 }} + + clean-benchmark: + if: always() + name: Clean Benchmarking + needs: [ list-version, benchmark-test ] + 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: "001-${{ strategy.job-index }}" \ No newline at end of file diff --git a/.github/workflows/rerun-workflow.yml b/.github/workflows/rerun-workflow.yml new file mode 100644 index 0000000..6c31950 --- /dev/null +++ b/.github/workflows/rerun-workflow.yml @@ -0,0 +1,22 @@ +name: Rerun workflow +on: + workflow_run: + workflows: ["Build and Run Tests by Maven" , "Build and Run Tests by Bazel"] + types: + - completed + +permissions: + actions: write + +jobs: + rerun: + if: github.event.workflow_run.conclusion == 'failure' && fromJSON(github.event.workflow_run.run_attempt) < 3 + runs-on: ubuntu-latest + steps: + - name: rerun ${{ github.event.workflow_run.id }} + env: + GH_REPO: ${{ github.repository }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh run watch ${{ github.event.workflow_run.id }} > /dev/null 2>&1 + gh run rerun ${{ github.event.workflow_run.id }} --failed \ No newline at end of file diff --git a/.github/workflows/snapshot-automation.yml b/.github/workflows/snapshot-automation.yml new file mode 100644 index 0000000..9b29758 --- /dev/null +++ b/.github/workflows/snapshot-automation.yml @@ -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. + +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: + if: github.repository == 'apache/rocketmq' + 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@v4 + with: + distribution: "corretto" + 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@v4 + 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@v4 + 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@v4 + name: Upload distribution tar + with: + name: versionlist + path: rocketmq-docker/image-build-ci/versionlist/* + + list-version: + if: > + github.repository == 'apache/rocketmq' && + 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@v4 + 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@v4 + 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@v4 + with: + java-version: 8 + distribution: "corretto" + 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 0000000..ca1a153 --- /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 new file mode 100644 index 0000000..c87f81e --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +*dependency-reduced-pom.xml +.classpath +.project +.settings/ +target/ +devenv +*.log.* +*.iml +.idea/ +*.versionsBackup +!NOTICE-BIN +!LICENSE-BIN +.DS_Store +localbin +nohup.out +bazel-out +bazel-bin +bazel-rocketmq +bazel-testlogs +.vscode +MODULE.bazel.lock \ No newline at end of file diff --git a/.licenserc.yaml b/.licenserc.yaml new file mode 100644 index 0000000..f74fb8c --- /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/BUILD.bazel b/BUILD.bazel new file mode 100644 index 0000000..ba33a9e --- /dev/null +++ b/BUILD.bazel @@ -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. +# +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", + "@maven//:org_mockito_mockito_junit_jupiter", + ], +) diff --git a/BUILDING b/BUILDING new file mode 100644 index 0000000..c6e23f5 --- /dev/null +++ b/BUILDING @@ -0,0 +1,37 @@ +Build Instructions for Apache RocketMQ + +==================================================== + +(1) Prerequisites + + JDK 1.7+ is required in order to compile and run RocketMQ. + + RocketMQ utilizes Maven as a distribution management and packaging tool. Version 3.0.3 or later is required. + Maven installation and configuration instructions can be found here: + + http://maven.apache.org/run-maven/index.html + + +(2) Run test cases + + Execute the following command in order to compile and run test cases of each components: + + $ mvn test + + +(3) Import projects to Eclipse IDE + + First, generate eclipse project files: + + $ mvn -U eclipse:eclipse + + Then, import to eclipse by specifying the root directory of the project via: + + [File] > [Import] > [Existing Projects into Workspace]. + + +(4) Build distribution packages + + Execute the following command in order to build the tar.gz packages and install JAR into local repository: + + $ mvn -Prelease-all -DskipTests clean install -U \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f3386e8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,48 @@ +## How To Contribute + +We are always very happy to have contributions, whether for trivial cleanups or big new features. +We want to have high quality, well documented codes for each programming language, as well as the surrounding [ecosystem](https://github.com/apache/rocketmq-externals) of integration tools that people use with RocketMQ. + +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 [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 SDK(C++\.Net\Php\Python\Go\Node.js) +#### 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 +###### 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 new file mode 100644 index 0000000..f49a4e1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "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 (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. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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/MODULE.bazel b/MODULE.bazel new file mode 100644 index 0000000..15fc5c6 --- /dev/null +++ b/MODULE.bazel @@ -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. +# +############################################################################### +# Bazel now uses Bzlmod by default to manage external dependencies. +# Please consider migrating your external dependencies from WORKSPACE to MODULE.bazel. +# +# For more details, please check https://github.com/bazelbuild/bazel/issues/18958 +############################################################################### diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..6e7ed4a --- /dev/null +++ b/NOTICE @@ -0,0 +1,5 @@ +Apache RocketMQ +Copyright 2016-2025 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/README.md b/README.md new file mode 100644 index 0000000..322058f --- /dev/null +++ b/README.md @@ -0,0 +1,250 @@ +## 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]][percentage-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: + +* 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 +* Reliable FIFO and strict ordered messaging in the same queue +* Efficient pull and push consumption model +* Million-level message accumulation capacity in a single queue +* 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.3.3/rocketmq-all-5.3.3-bin-release.zip) to download the 5.3.3 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.3.3/rocketmq-all-5.3.3-bin-release.zip + +# Unpack the release +$ unzip rocketmq-all-5.3.3-bin-release.zip +``` + +Prepare a terminal and change to the extracted `bin` directory: +```shell +$ cd rocketmq-all-5.3.3-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 type=bind,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): Incubator 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: +* Rips: +* Ask: +* Slack: + + +---------- + + + +## Contributing +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 +[percentage-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 0000000..9125a67 --- /dev/null +++ b/WORKSPACE @@ -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. +# +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", + "com.alibaba.fastjson2:fastjson2:2.0.43", + "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.4", + "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", + "org.mockito:mockito-junit-jupiter:4.11.0", + "com.alibaba.fastjson2:fastjson2:2.0.43", + "org.junit.jupiter:junit-jupiter-api:5.9.1", + ], + 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/auth/BUILD.bazel b/auth/BUILD.bazel new file mode 100644 index 0000000..bc15ca3 --- /dev/null +++ b/auth/BUILD.bazel @@ -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. +# +load("//bazel:GenTestRules.bzl", "GenTestRules") + +java_library( + name = "auth", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//common", + "//remoting", + "//srvutil", + "//client", + "@maven//:commons_codec_commons_codec", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:commons_collections_commons_collections", + "@maven//:com_alibaba_fastjson2_fastjson2", + "@maven//:org_apache_rocketmq_rocketmq_proto", + "@maven//:org_slf4j_slf4j_api", + "@maven//:com_github_ben_manes_caffeine_caffeine", + "@maven//:io_grpc_grpc_api", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:com_google_protobuf_protobuf_java_util", + "@maven//:io_netty_netty_all", + "@maven//:com_google_guava_guava", + "@maven//:org_apache_rocketmq_rocketmq_rocksdb", + ], +) + +java_library( + name = "tests", + srcs = glob(["src/test/java/**/*.java"]), + resources = glob(["src/test/resources/**/*.yml"]), + visibility = ["//visibility:public"], + deps = [ + ":auth", + "//:test_deps", + "//common", + "//remoting", + "//client", + "@maven//:commons_codec_commons_codec", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:commons_collections_commons_collections", + "@maven//:com_alibaba_fastjson2_fastjson2", + "@maven//:org_apache_rocketmq_rocketmq_proto", + "@maven//:org_slf4j_slf4j_api", + "@maven//:com_github_ben_manes_caffeine_caffeine", + "@maven//:io_grpc_grpc_api", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:com_google_protobuf_protobuf_java_util", + "@maven//:io_netty_netty_all", + "@maven//:com_google_guava_guava", + "@maven//:org_apache_rocketmq_rocketmq_rocksdb", + ], +) + +GenTestRules( + name = "GeneratedTestRules", + test_files = glob(["src/test/java/**/*Test.java"]), + deps = [ + ":tests", + ], +) diff --git a/auth/pom.xml b/auth/pom.xml new file mode 100644 index 0000000..3a1211a --- /dev/null +++ b/auth/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + org.apache.rocketmq + rocketmq-all + 5.3.4-SNAPSHOT + + rocketmq-auth + rocketmq-auth ${project.version} + + + ${basedir}/.. + + + + + ${project.groupId} + rocketmq-proto + + + ${project.groupId} + rocketmq-client + + + commons-codec + commons-codec + + + org.apache.commons + commons-lang3 + + + com.google.protobuf + protobuf-java-util + + + org.slf4j + slf4j-api + + + com.github.ben-manes.caffeine + caffeine + + + org.checkerframework + checker-qual + + + + + junit + junit + + + + + + + maven-surefire-plugin + ${maven-surefire-plugin.version} + + 1 + false + + + + + diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authentication/AuthenticationEvaluator.java b/auth/src/main/java/org/apache/rocketmq/auth/authentication/AuthenticationEvaluator.java new file mode 100644 index 0000000..3b7d45d --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authentication/AuthenticationEvaluator.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.auth.authentication; + +import java.util.function.Supplier; +import org.apache.rocketmq.auth.authentication.context.AuthenticationContext; +import org.apache.rocketmq.auth.authentication.factory.AuthenticationFactory; +import org.apache.rocketmq.auth.authentication.strategy.AuthenticationStrategy; +import org.apache.rocketmq.auth.config.AuthConfig; + +public class AuthenticationEvaluator { + + private final AuthenticationStrategy authenticationStrategy; + + public AuthenticationEvaluator(AuthConfig authConfig) { + this(authConfig, null); + } + + public AuthenticationEvaluator(AuthConfig authConfig, Supplier metadataService) { + this.authenticationStrategy = AuthenticationFactory.getStrategy(authConfig, metadataService); + } + + public void evaluate(AuthenticationContext context) { + if (context == null) { + return; + } + this.authenticationStrategy.evaluate(context); + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authentication/builder/AuthenticationContextBuilder.java b/auth/src/main/java/org/apache/rocketmq/auth/authentication/builder/AuthenticationContextBuilder.java new file mode 100644 index 0000000..0176f01 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authentication/builder/AuthenticationContextBuilder.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.auth.authentication.builder; + +import com.google.protobuf.GeneratedMessageV3; +import io.grpc.Metadata; +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public interface AuthenticationContextBuilder { + + AuthenticationContext build(Metadata metadata, GeneratedMessageV3 request); + + AuthenticationContext build(ChannelHandlerContext context, RemotingCommand request); +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authentication/builder/DefaultAuthenticationContextBuilder.java b/auth/src/main/java/org/apache/rocketmq/auth/authentication/builder/DefaultAuthenticationContextBuilder.java new file mode 100644 index 0000000..c1e970f --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authentication/builder/DefaultAuthenticationContextBuilder.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.auth.authentication.builder; + +import com.google.protobuf.GeneratedMessageV3; +import io.grpc.Metadata; +import io.netty.channel.ChannelHandlerContext; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.acl.common.AclUtils; +import org.apache.rocketmq.acl.common.SessionCredentials; +import org.apache.rocketmq.auth.authentication.context.DefaultAuthenticationContext; +import org.apache.rocketmq.auth.authentication.exception.AuthenticationException; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.constant.CommonConstants; +import org.apache.rocketmq.common.constant.GrpcConstants; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public class DefaultAuthenticationContextBuilder implements AuthenticationContextBuilder { + + private static final String CREDENTIAL = "Credential"; + private static final String SIGNATURE = "Signature"; + + @Override + public DefaultAuthenticationContext build(Metadata metadata, GeneratedMessageV3 request) { + try { + DefaultAuthenticationContext context = new DefaultAuthenticationContext(); + context.setChannelId(metadata.get(GrpcConstants.CHANNEL_ID)); + context.setRpcCode(request.getDescriptorForType().getFullName()); + String authorization = metadata.get(GrpcConstants.AUTHORIZATION); + if (StringUtils.isEmpty(authorization)) { + return context; + } + String datetime = metadata.get(GrpcConstants.DATE_TIME); + if (StringUtils.isEmpty(datetime)) { + throw new AuthenticationException("datetime is null."); + } + + String[] result = authorization.split(CommonConstants.SPACE, 2); + if (result.length != 2) { + throw new AuthenticationException("authentication header is incorrect."); + } + String[] keyValues = result[1].split(CommonConstants.COMMA); + for (String keyValue : keyValues) { + String[] kv = keyValue.trim().split(CommonConstants.EQUAL, 2); + int kvLength = kv.length; + if (kv.length != 2) { + throw new AuthenticationException("authentication keyValues length is incorrect, actual length={}.", kvLength); + } + String authItem = kv[0]; + if (CREDENTIAL.equals(authItem)) { + String[] credential = kv[1].split(CommonConstants.SLASH); + int credentialActualLength = credential.length; + if (credentialActualLength == 0) { + throw new AuthenticationException("authentication credential length is incorrect, actual length={}.", credentialActualLength); + } + context.setUsername(credential[0]); + continue; + } + if (SIGNATURE.equals(authItem)) { + context.setSignature(this.hexToBase64(kv[1])); + } + } + + context.setContent(datetime.getBytes(StandardCharsets.UTF_8)); + + return context; + } catch (AuthenticationException e) { + throw e; + } catch (Throwable e) { + throw new AuthenticationException("create authentication context error.", e); + } + } + + @Override + public DefaultAuthenticationContext build(ChannelHandlerContext context, RemotingCommand request) { + HashMap fields = request.getExtFields(); + DefaultAuthenticationContext result = new DefaultAuthenticationContext(); + result.setChannelId(context.channel().id().asLongText()); + result.setRpcCode(String.valueOf(request.getCode())); + if (MapUtils.isEmpty(fields)) { + return result; + } + if (!fields.containsKey(SessionCredentials.ACCESS_KEY)) { + return result; + } + result.setUsername(fields.get(SessionCredentials.ACCESS_KEY)); + result.setSignature(fields.get(SessionCredentials.SIGNATURE)); + // Content + SortedMap map = new TreeMap<>(); + for (Map.Entry entry : fields.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()); + } + } + result.setContent(AclUtils.combineRequestContent(request, map)); + return result; + } + + public String hexToBase64(String input) throws DecoderException { + byte[] bytes = Hex.decodeHex(input); + return Base64.encodeBase64String(bytes); + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authentication/chain/DefaultAuthenticationHandler.java b/auth/src/main/java/org/apache/rocketmq/auth/authentication/chain/DefaultAuthenticationHandler.java new file mode 100644 index 0000000..4b50de7 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authentication/chain/DefaultAuthenticationHandler.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.auth.authentication.chain; + +import java.security.MessageDigest; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.acl.common.AclSigner; +import org.apache.rocketmq.auth.authentication.context.DefaultAuthenticationContext; +import org.apache.rocketmq.auth.authentication.enums.UserStatus; +import org.apache.rocketmq.auth.authentication.exception.AuthenticationException; +import org.apache.rocketmq.auth.authentication.factory.AuthenticationFactory; +import org.apache.rocketmq.auth.authentication.model.User; +import org.apache.rocketmq.auth.authentication.provider.AuthenticationMetadataProvider; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.common.chain.Handler; +import org.apache.rocketmq.common.chain.HandlerChain; + +public class DefaultAuthenticationHandler implements Handler> { + + private final AuthenticationMetadataProvider authenticationMetadataProvider; + + public DefaultAuthenticationHandler(AuthConfig config, Supplier metadataService) { + this.authenticationMetadataProvider = AuthenticationFactory.getMetadataProvider(config, metadataService); + } + + @Override + public CompletableFuture handle(DefaultAuthenticationContext context, + HandlerChain> chain) { + return getUser(context).thenAccept(user -> doAuthenticate(context, user)); + } + + protected CompletableFuture getUser(DefaultAuthenticationContext context) { + if (this.authenticationMetadataProvider == null) { + throw new AuthenticationException("The authenticationMetadataProvider is not configured"); + } + if (StringUtils.isEmpty(context.getUsername())) { + throw new AuthenticationException("username cannot be null."); + } + return this.authenticationMetadataProvider.getUser(context.getUsername()); + } + + protected void doAuthenticate(DefaultAuthenticationContext context, User user) { + if (user == null) { + throw new AuthenticationException("User:{} is not found.", context.getUsername()); + } + if (user.getUserStatus() == UserStatus.DISABLE) { + throw new AuthenticationException("User:{} is disabled.", context.getUsername()); + } + String signature = AclSigner.calSignature(context.getContent(), user.getPassword()); + if (context.getSignature() == null + || !MessageDigest.isEqual(signature.getBytes(AclSigner.DEFAULT_CHARSET), context.getSignature().getBytes(AclSigner.DEFAULT_CHARSET))) { + throw new AuthenticationException("check signature failed."); + } + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authentication/context/AuthenticationContext.java b/auth/src/main/java/org/apache/rocketmq/auth/authentication/context/AuthenticationContext.java new file mode 100644 index 0000000..7c10f04 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authentication/context/AuthenticationContext.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.auth.authentication.context; + +import java.util.HashMap; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; + +public abstract class AuthenticationContext { + + private String channelId; + + private String rpcCode; + + private Map extInfo; + + public String getChannelId() { + return channelId; + } + + public void setChannelId(String channelId) { + this.channelId = channelId; + } + + public String getRpcCode() { + return rpcCode; + } + + public void setRpcCode(String rpcCode) { + this.rpcCode = rpcCode; + } + + @SuppressWarnings("unchecked") + public T getExtInfo(String key) { + if (StringUtils.isBlank(key)) { + return null; + } + if (this.extInfo == null) { + return null; + } + Object value = this.extInfo.get(key); + if (value == null) { + return null; + } + return (T) value; + } + + public void setExtInfo(String key, Object value) { + if (StringUtils.isBlank(key) || value == null) { + return; + } + if (this.extInfo == null) { + this.extInfo = new HashMap<>(); + } + this.extInfo.put(key, value); + } + + public boolean hasExtInfo(String key) { + Object value = getExtInfo(key); + return value != null; + } + + public Map getExtInfo() { + return extInfo; + } + + public void setExtInfo(Map extInfo) { + this.extInfo = extInfo; + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authentication/context/DefaultAuthenticationContext.java b/auth/src/main/java/org/apache/rocketmq/auth/authentication/context/DefaultAuthenticationContext.java new file mode 100644 index 0000000..a6fff86 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authentication/context/DefaultAuthenticationContext.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.auth.authentication.context; + +public class DefaultAuthenticationContext extends AuthenticationContext { + + private String username; + + private byte[] content; + + private String signature; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public byte[] getContent() { + return content; + } + + public void setContent(byte[] content) { + this.content = content; + } + + public String getSignature() { + return signature; + } + + public void setSignature(String signature) { + this.signature = signature; + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authentication/enums/SubjectType.java b/auth/src/main/java/org/apache/rocketmq/auth/authentication/enums/SubjectType.java new file mode 100644 index 0000000..64a40f9 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authentication/enums/SubjectType.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.auth.authentication.enums; + +import com.alibaba.fastjson2.annotation.JSONField; +import org.apache.commons.lang3.StringUtils; + +public enum SubjectType { + + USER((byte) 1, "User"); + + @JSONField(value = true) + private final byte code; + private final String name; + + SubjectType(byte code, String name) { + this.code = code; + this.name = name; + } + + public static SubjectType getByName(String name) { + for (SubjectType subjectType : SubjectType.values()) { + if (StringUtils.equalsIgnoreCase(subjectType.getName(), name)) { + return subjectType; + } + } + return null; + } + + public byte getCode() { + return code; + } + + public String getName() { + return name; + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authentication/enums/UserStatus.java b/auth/src/main/java/org/apache/rocketmq/auth/authentication/enums/UserStatus.java new file mode 100644 index 0000000..9bb25a2 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authentication/enums/UserStatus.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.auth.authentication.enums; + +import com.alibaba.fastjson2.annotation.JSONField; +import org.apache.commons.lang3.StringUtils; + +public enum UserStatus { + + ENABLE((byte) 1, "enable"), + + DISABLE((byte) 2, "disable"); + + @JSONField(value = true) + private final byte code; + + private final String name; + + UserStatus(byte code, String name) { + this.code = code; + this.name = name; + } + + public static UserStatus getByName(String name) { + for (UserStatus subjectType : UserStatus.values()) { + if (StringUtils.equalsIgnoreCase(subjectType.getName(), name)) { + return subjectType; + } + } + return null; + } + + public byte getCode() { + return code; + } + + public String getName() { + return name; + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authentication/enums/UserType.java b/auth/src/main/java/org/apache/rocketmq/auth/authentication/enums/UserType.java new file mode 100644 index 0000000..6dc786f --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authentication/enums/UserType.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.auth.authentication.enums; + +import com.alibaba.fastjson2.annotation.JSONField; +import org.apache.commons.lang3.StringUtils; + +public enum UserType { + + SUPER((byte) 1, "Super"), + + NORMAL((byte) 2, "Normal"); + + @JSONField(value = true) + private final byte code; + + private final String name; + + UserType(byte code, String name) { + this.code = code; + this.name = name; + } + + public static UserType getByName(String name) { + for (UserType subjectType : UserType.values()) { + if (StringUtils.equalsIgnoreCase(subjectType.getName(), name)) { + return subjectType; + } + } + return null; + } + + public byte getCode() { + return code; + } + + public String getName() { + return name; + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authentication/exception/AuthenticationException.java b/auth/src/main/java/org/apache/rocketmq/auth/authentication/exception/AuthenticationException.java new file mode 100644 index 0000000..9b66c81 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authentication/exception/AuthenticationException.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.auth.authentication.exception; + +import org.slf4j.helpers.MessageFormatter; + +public class AuthenticationException extends RuntimeException { + + public AuthenticationException(String message) { + super(message); + } + + public AuthenticationException(String message, Throwable cause) { + super(message, cause); + } + + public AuthenticationException(String messagePattern, Object... argArray) { + super(MessageFormatter.arrayFormat(messagePattern, argArray).getMessage()); + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authentication/factory/AuthenticationFactory.java b/auth/src/main/java/org/apache/rocketmq/auth/authentication/factory/AuthenticationFactory.java new file mode 100644 index 0000000..3ba82ad --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authentication/factory/AuthenticationFactory.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.auth.authentication.factory; + +import com.google.protobuf.GeneratedMessageV3; +import io.grpc.Metadata; +import io.netty.channel.ChannelHandlerContext; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.auth.authentication.AuthenticationEvaluator; +import org.apache.rocketmq.auth.authentication.context.AuthenticationContext; +import org.apache.rocketmq.auth.authentication.manager.AuthenticationMetadataManager; +import org.apache.rocketmq.auth.authentication.manager.AuthenticationMetadataManagerImpl; +import org.apache.rocketmq.auth.authentication.provider.AuthenticationMetadataProvider; +import org.apache.rocketmq.auth.authentication.provider.AuthenticationProvider; +import org.apache.rocketmq.auth.authentication.provider.DefaultAuthenticationProvider; +import org.apache.rocketmq.auth.authentication.strategy.AuthenticationStrategy; +import org.apache.rocketmq.auth.authentication.strategy.StatelessAuthenticationStrategy; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public class AuthenticationFactory { + + private static final Map INSTANCE_MAP = new HashMap<>(); + private static final String PROVIDER_PREFIX = "PROVIDER_"; + private static final String METADATA_PROVIDER_PREFIX = "METADATA_PROVIDER_"; + private static final String EVALUATOR_PREFIX = "EVALUATOR_"; + + @SuppressWarnings("unchecked") + public static AuthenticationProvider getProvider(AuthConfig config) { + if (config == null) { + return null; + } + return computeIfAbsent(PROVIDER_PREFIX + config.getConfigName(), key -> { + try { + Class> clazz = + DefaultAuthenticationProvider.class; + if (StringUtils.isNotBlank(config.getAuthenticationProvider())) { + clazz = (Class>) Class.forName(config.getAuthenticationProvider()); + } + return (AuthenticationProvider) clazz.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException("Failed to load the authentication provider.", e); + } + }); + } + + public static AuthenticationMetadataProvider getMetadataProvider(AuthConfig config) { + return getMetadataProvider(config, null); + } + + public static AuthenticationMetadataManager getMetadataManager(AuthConfig config) { + return new AuthenticationMetadataManagerImpl(config); + } + + @SuppressWarnings("unchecked") + public static AuthenticationMetadataProvider getMetadataProvider(AuthConfig config, Supplier metadataService) { + if (config == null) { + return null; + } + return computeIfAbsent(METADATA_PROVIDER_PREFIX + config.getConfigName(), key -> { + try { + if (StringUtils.isBlank(config.getAuthenticationMetadataProvider())) { + return null; + } + Class clazz = (Class) + Class.forName(config.getAuthenticationMetadataProvider()); + AuthenticationMetadataProvider result = clazz.getDeclaredConstructor().newInstance(); + result.initialize(config, metadataService); + return result; + } catch (Exception e) { + throw new RuntimeException("Failed to load the authentication metadata provider", e); + } + }); + } + + public static AuthenticationEvaluator getEvaluator(AuthConfig config) { + return computeIfAbsent(EVALUATOR_PREFIX + config.getConfigName(), key -> new AuthenticationEvaluator(config)); + } + + public static AuthenticationEvaluator getEvaluator(AuthConfig config, Supplier metadataService) { + return computeIfAbsent(EVALUATOR_PREFIX + config.getConfigName(), key -> new AuthenticationEvaluator(config, metadataService)); + } + + @SuppressWarnings("unchecked") + public static AuthenticationStrategy getStrategy(AuthConfig config, Supplier metadataService) { + try { + Class clazz = StatelessAuthenticationStrategy.class; + if (StringUtils.isNotBlank(config.getAuthenticationStrategy())) { + clazz = (Class) Class.forName(config.getAuthenticationStrategy()); + } + return clazz.getDeclaredConstructor(AuthConfig.class, Supplier.class).newInstance(config, metadataService); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static AuthenticationContext newContext(AuthConfig config, Metadata metadata, GeneratedMessageV3 request) { + AuthenticationProvider authenticationProvider = getProvider(config); + if (authenticationProvider == null) { + return null; + } + return authenticationProvider.newContext(metadata, request); + } + + public static AuthenticationContext newContext(AuthConfig config, ChannelHandlerContext context, + RemotingCommand command) { + AuthenticationProvider authenticationProvider = getProvider(config); + if (authenticationProvider == null) { + return null; + } + return authenticationProvider.newContext(context, command); + } + + @SuppressWarnings("unchecked") + private static V computeIfAbsent(String key, Function function) { + Object result = null; + if (INSTANCE_MAP.containsKey(key)) { + result = INSTANCE_MAP.get(key); + } + if (result == null) { + synchronized (INSTANCE_MAP) { + if (INSTANCE_MAP.containsKey(key)) { + result = INSTANCE_MAP.get(key); + } + if (result == null) { + result = function.apply(key); + if (result != null) { + INSTANCE_MAP.put(key, result); + } + } + } + } + return result != null ? (V) result : null; + } +} \ No newline at end of file diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authentication/manager/AuthenticationMetadataManager.java b/auth/src/main/java/org/apache/rocketmq/auth/authentication/manager/AuthenticationMetadataManager.java new file mode 100644 index 0000000..b390643 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authentication/manager/AuthenticationMetadataManager.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.auth.authentication.manager; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.auth.authentication.model.User; +import org.apache.rocketmq.auth.config.AuthConfig; + +public interface AuthenticationMetadataManager { + + void shutdown(); + + void initUser(AuthConfig authConfig); + + CompletableFuture createUser(User user); + + CompletableFuture updateUser(User user); + + CompletableFuture deleteUser(String username); + + CompletableFuture getUser(String username); + + CompletableFuture> listUser(String filter); + + CompletableFuture isSuperUser(String username); +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authentication/manager/AuthenticationMetadataManagerImpl.java b/auth/src/main/java/org/apache/rocketmq/auth/authentication/manager/AuthenticationMetadataManagerImpl.java new file mode 100644 index 0000000..39620ca --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authentication/manager/AuthenticationMetadataManagerImpl.java @@ -0,0 +1,222 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.auth.authentication.manager; + +import com.alibaba.fastjson2.JSON; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.acl.common.SessionCredentials; +import org.apache.rocketmq.auth.authentication.enums.UserStatus; +import org.apache.rocketmq.auth.authentication.enums.UserType; +import org.apache.rocketmq.auth.authentication.exception.AuthenticationException; +import org.apache.rocketmq.auth.authentication.factory.AuthenticationFactory; +import org.apache.rocketmq.auth.authentication.model.User; +import org.apache.rocketmq.auth.authentication.provider.AuthenticationMetadataProvider; +import org.apache.rocketmq.auth.authorization.factory.AuthorizationFactory; +import org.apache.rocketmq.auth.authorization.provider.AuthorizationMetadataProvider; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.common.utils.ExceptionUtils; + +public class AuthenticationMetadataManagerImpl implements AuthenticationMetadataManager { + + private final AuthenticationMetadataProvider authenticationMetadataProvider; + + private final AuthorizationMetadataProvider authorizationMetadataProvider; + + public AuthenticationMetadataManagerImpl(AuthConfig authConfig) { + this.authenticationMetadataProvider = AuthenticationFactory.getMetadataProvider(authConfig); + this.authorizationMetadataProvider = AuthorizationFactory.getMetadataProvider(authConfig); + this.initUser(authConfig); + } + + @Override + public void shutdown() { + if (this.authenticationMetadataProvider != null) { + this.authenticationMetadataProvider.shutdown(); + } + if (this.authorizationMetadataProvider != null) { + this.authorizationMetadataProvider.shutdown(); + } + } + + @Override + public void initUser(AuthConfig authConfig) { + if (authConfig == null) { + return; + } + if (StringUtils.isNotBlank(authConfig.getInitAuthenticationUser())) { + try { + User initUser = JSON.parseObject(authConfig.getInitAuthenticationUser(), User.class); + initUser.setUserType(UserType.SUPER); + this.getUser(initUser.getUsername()).thenCompose(user -> { + if (user != null) { + return CompletableFuture.completedFuture(null); + } + return this.createUser(initUser); + }).join(); + } catch (Exception e) { + throw new AuthenticationException("Init authentication user error.", e); + } + } + if (StringUtils.isNotBlank(authConfig.getInnerClientAuthenticationCredentials())) { + try { + SessionCredentials credentials = JSON.parseObject(authConfig.getInnerClientAuthenticationCredentials(), SessionCredentials.class); + User innerUser = User.of(credentials.getAccessKey(), credentials.getSecretKey(), UserType.SUPER); + this.getUser(innerUser.getUsername()).thenCompose(user -> { + if (user != null) { + return CompletableFuture.completedFuture(null); + } + return this.createUser(innerUser); + }).join(); + } catch (Exception e) { + throw new AuthenticationException("Init inner client authentication credentials error", e); + } + } + } + + @Override + public CompletableFuture createUser(User user) { + CompletableFuture result = new CompletableFuture<>(); + try { + this.validate(user, true); + if (user.getUserType() == null) { + user.setUserType(UserType.NORMAL); + } + if (user.getUserStatus() == null) { + user.setUserStatus(UserStatus.ENABLE); + } + result = this.getAuthenticationMetadataProvider().getUser(user.getUsername()).thenCompose(old -> { + if (old != null) { + throw new AuthenticationException("The user is existed"); + } + return this.getAuthenticationMetadataProvider().createUser(user); + }); + } catch (Exception e) { + this.handleException(e, result); + } + return result; + } + + @Override + public CompletableFuture updateUser(User user) { + CompletableFuture result = new CompletableFuture<>(); + try { + this.validate(user, false); + result = this.getAuthenticationMetadataProvider().getUser(user.getUsername()).thenCompose(old -> { + if (old == null) { + throw new AuthenticationException("The user is not exist"); + } + if (StringUtils.isNotBlank(user.getPassword())) { + old.setPassword(user.getPassword()); + } + if (user.getUserType() != null) { + old.setUserType(user.getUserType()); + } + if (user.getUserStatus() != null) { + old.setUserStatus(user.getUserStatus()); + } + return this.getAuthenticationMetadataProvider().updateUser(old); + }); + } catch (Exception e) { + this.handleException(e, result); + } + return result; + } + + @Override + public CompletableFuture deleteUser(String username) { + CompletableFuture result = new CompletableFuture<>(); + try { + if (StringUtils.isBlank(username)) { + throw new AuthenticationException("username can not be blank"); + } + CompletableFuture deleteUser = this.getAuthenticationMetadataProvider().deleteUser(username); + CompletableFuture deleteAcl = this.getAuthorizationMetadataProvider().deleteAcl(User.of(username)); + return CompletableFuture.allOf(deleteUser, deleteAcl); + } catch (Exception e) { + this.handleException(e, result); + } + return result; + } + + @Override + public CompletableFuture getUser(String username) { + CompletableFuture result = new CompletableFuture<>(); + try { + if (StringUtils.isBlank(username)) { + throw new AuthenticationException("username can not be blank"); + } + result = this.getAuthenticationMetadataProvider().getUser(username); + } catch (Exception e) { + this.handleException(e, result); + } + return result; + } + + @Override + public CompletableFuture> listUser(String filter) { + CompletableFuture> result = new CompletableFuture<>(); + try { + result = this.getAuthenticationMetadataProvider().listUser(filter); + } catch (Exception e) { + this.handleException(e, result); + } + return result; + } + + @Override + public CompletableFuture isSuperUser(String username) { + return this.getUser(username).thenApply(user -> { + if (user == null) { + throw new AuthenticationException("User:{} is not found", username); + } + return user.getUserType() == UserType.SUPER; + }); + } + + private void validate(User user, boolean isCreate) { + if (user == null) { + throw new AuthenticationException("user can not be null"); + } + if (StringUtils.isBlank(user.getUsername())) { + throw new AuthenticationException("username can not be blank"); + } + if (isCreate && StringUtils.isBlank(user.getPassword())) { + throw new AuthenticationException("password can not be blank"); + } + } + + private void handleException(Exception e, CompletableFuture result) { + Throwable throwable = ExceptionUtils.getRealException(e); + result.completeExceptionally(throwable); + } + + private AuthenticationMetadataProvider getAuthenticationMetadataProvider() { + if (authenticationMetadataProvider == null) { + throw new IllegalStateException("The authenticationMetadataProvider is not configured"); + } + return authenticationMetadataProvider; + } + + private AuthorizationMetadataProvider getAuthorizationMetadataProvider() { + if (authorizationMetadataProvider == null) { + throw new IllegalStateException("The authorizationMetadataProvider is not configured"); + } + return authorizationMetadataProvider; + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authentication/model/Subject.java b/auth/src/main/java/org/apache/rocketmq/auth/authentication/model/Subject.java new file mode 100644 index 0000000..25b5dcb --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authentication/model/Subject.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.auth.authentication.model; + +import com.alibaba.fastjson2.annotation.JSONField; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.auth.authentication.enums.SubjectType; +import org.apache.rocketmq.common.constant.CommonConstants; + +public interface Subject { + + @JSONField(serialize = false) + String getSubjectKey(); + + SubjectType getSubjectType(); + + default boolean isSubject(SubjectType subjectType) { + return subjectType == this.getSubjectType(); + } + + @SuppressWarnings("unchecked") + static T of(String subjectKey) { + String type = StringUtils.substringBefore(subjectKey, CommonConstants.COLON); + SubjectType subjectType = SubjectType.getByName(type); + if (subjectType == null) { + return null; + } + if (subjectType == SubjectType.USER) { + return (T) User.of(StringUtils.substringAfter(subjectKey, CommonConstants.COLON)); + } + return null; + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authentication/model/User.java b/auth/src/main/java/org/apache/rocketmq/auth/authentication/model/User.java new file mode 100644 index 0000000..4b009ca --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authentication/model/User.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.auth.authentication.model; + +import org.apache.rocketmq.auth.authentication.enums.SubjectType; +import org.apache.rocketmq.auth.authentication.enums.UserStatus; +import org.apache.rocketmq.auth.authentication.enums.UserType; +import org.apache.rocketmq.common.constant.CommonConstants; + +public class User implements Subject { + + private String username; + + private String password; + + private UserType userType; + + private UserStatus userStatus; + + public static User of(String username) { + User user = new User(); + user.setUsername(username); + return user; + } + + public static User of(String username, String password) { + User user = new User(); + user.setUsername(username); + user.setPassword(password); + return user; + } + + public static User of(String username, String password, UserType userType) { + User user = new User(); + user.setUsername(username); + user.setPassword(password); + user.setUserType(userType); + return user; + } + + @Override + public String getSubjectKey() { + return this.getSubjectType().getName() + CommonConstants.COLON + this.username; + } + + @Override + public SubjectType getSubjectType() { + return SubjectType.USER; + } + + 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; + } + + public UserType getUserType() { + return userType; + } + + public void setUserType(UserType userType) { + this.userType = userType; + } + + public UserStatus getUserStatus() { + return userStatus; + } + + public void setUserStatus(UserStatus userStatus) { + this.userStatus = userStatus; + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authentication/provider/AuthenticationMetadataProvider.java b/auth/src/main/java/org/apache/rocketmq/auth/authentication/provider/AuthenticationMetadataProvider.java new file mode 100644 index 0000000..59c3ec1 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authentication/provider/AuthenticationMetadataProvider.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.auth.authentication.provider; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import org.apache.rocketmq.auth.authentication.model.User; +import org.apache.rocketmq.auth.config.AuthConfig; + +public interface AuthenticationMetadataProvider { + + void initialize(AuthConfig authConfig, Supplier metadataService); + + void shutdown(); + + CompletableFuture createUser(User user); + + CompletableFuture deleteUser(String username); + + CompletableFuture updateUser(User user); + + CompletableFuture getUser(String username); + + CompletableFuture> listUser(String filter); +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authentication/provider/AuthenticationProvider.java b/auth/src/main/java/org/apache/rocketmq/auth/authentication/provider/AuthenticationProvider.java new file mode 100644 index 0000000..61660b5 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authentication/provider/AuthenticationProvider.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.auth.authentication.provider; + +import com.google.protobuf.GeneratedMessageV3; +import io.grpc.Metadata; +import io.netty.channel.ChannelHandlerContext; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public interface AuthenticationProvider { + + void initialize(AuthConfig config, Supplier metadataService); + + CompletableFuture authenticate(AuthenticationContext context); + + AuthenticationContext newContext(Metadata metadata, GeneratedMessageV3 request); + + AuthenticationContext newContext(ChannelHandlerContext context, RemotingCommand command); +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authentication/provider/DefaultAuthenticationProvider.java b/auth/src/main/java/org/apache/rocketmq/auth/authentication/provider/DefaultAuthenticationProvider.java new file mode 100644 index 0000000..98e7ede --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authentication/provider/DefaultAuthenticationProvider.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.auth.authentication.provider; + +import com.google.protobuf.GeneratedMessageV3; +import io.grpc.Metadata; +import io.netty.channel.ChannelHandlerContext; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.auth.authentication.builder.AuthenticationContextBuilder; +import org.apache.rocketmq.auth.authentication.builder.DefaultAuthenticationContextBuilder; +import org.apache.rocketmq.auth.authentication.context.DefaultAuthenticationContext; +import org.apache.rocketmq.auth.authentication.chain.DefaultAuthenticationHandler; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.common.chain.HandlerChain; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DefaultAuthenticationProvider implements AuthenticationProvider { + + protected final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_AUTH_AUDIT_LOGGER_NAME); + protected AuthConfig authConfig; + protected Supplier metadataService; + protected AuthenticationContextBuilder authenticationContextBuilder; + + @Override + public void initialize(AuthConfig config, Supplier metadataService) { + this.authConfig = config; + this.metadataService = metadataService; + this.authenticationContextBuilder = new DefaultAuthenticationContextBuilder(); + } + + @Override + public CompletableFuture authenticate(DefaultAuthenticationContext context) { + return this.newHandlerChain().handle(context) + .whenComplete((nil, ex) -> doAuditLog(context, ex)); + } + + @Override + public DefaultAuthenticationContext newContext(Metadata metadata, GeneratedMessageV3 request) { + return this.authenticationContextBuilder.build(metadata, request); + } + + @Override + public DefaultAuthenticationContext newContext(ChannelHandlerContext context, RemotingCommand command) { + return this.authenticationContextBuilder.build(context, command); + } + + protected HandlerChain> newHandlerChain() { + return HandlerChain.>create() + .addNext(new DefaultAuthenticationHandler(this.authConfig, metadataService)); + } + + protected void doAuditLog(DefaultAuthenticationContext context, Throwable ex) { + if (StringUtils.isBlank(context.getUsername())) { + return; + } + if (ex != null) { + log.info("[AUTHENTICATION] User:{} is authenticated failed with Signature = {}.", context.getUsername(), context.getSignature()); + } else { + log.debug("[AUTHENTICATION] User:{} is authenticated success with Signature = {}.", context.getUsername(), context.getSignature()); + } + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authentication/provider/LocalAuthenticationMetadataProvider.java b/auth/src/main/java/org/apache/rocketmq/auth/authentication/provider/LocalAuthenticationMetadataProvider.java new file mode 100644 index 0000000..dcf9061 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authentication/provider/LocalAuthenticationMetadataProvider.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.auth.authentication.provider; + +import com.alibaba.fastjson2.JSON; +import com.github.benmanes.caffeine.cache.CacheLoader; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.auth.authentication.exception.AuthenticationException; +import org.apache.rocketmq.auth.authentication.model.User; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.common.config.ConfigRocksDBStorage; +import org.apache.rocketmq.common.thread.ThreadPoolMonitor; +import org.rocksdb.RocksIterator; + +public class LocalAuthenticationMetadataProvider implements AuthenticationMetadataProvider { + + private ConfigRocksDBStorage storage; + + private LoadingCache userCache; + + @Override + public void initialize(AuthConfig authConfig, Supplier metadataService) { + this.storage = new ConfigRocksDBStorage(authConfig.getAuthConfigPath() + File.separator + "users"); + if (!this.storage.start()) { + throw new RuntimeException("Failed to load rocksdb for auth_user, please check whether it is occupied"); + } + + ThreadPoolExecutor cacheRefreshExecutor = ThreadPoolMonitor.createAndMonitor( + 1, + 1, + 1000 * 60, + TimeUnit.MILLISECONDS, + "UserCacheRefresh", + 100000 + ); + + this.userCache = Caffeine.newBuilder() + .maximumSize(authConfig.getUserCacheMaxNum()) + .expireAfterAccess(authConfig.getUserCacheExpiredSecond(), TimeUnit.SECONDS) + .refreshAfterWrite(authConfig.getUserCacheRefreshSecond(), TimeUnit.SECONDS) + .executor(cacheRefreshExecutor) + .build(new UserCacheLoader(this.storage)); + } + + @Override + public CompletableFuture createUser(User user) { + try { + byte[] keyBytes = user.getUsername().getBytes(StandardCharsets.UTF_8); + byte[] valueBytes = JSON.toJSONBytes(user); + this.storage.put(keyBytes, keyBytes.length, valueBytes); + this.storage.flushWAL(); + this.userCache.invalidate(user.getUsername()); + } catch (Exception e) { + throw new AuthenticationException("create user to RocksDB failed", e); + } + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture deleteUser(String username) { + try { + this.storage.delete(username.getBytes(StandardCharsets.UTF_8)); + this.storage.flushWAL(); + this.userCache.invalidate(username); + } catch (Exception e) { + throw new AuthenticationException("delete user from RocksDB failed", e); + } + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture updateUser(User user) { + try { + byte[] keyBytes = user.getUsername().getBytes(StandardCharsets.UTF_8); + byte[] valueBytes = JSON.toJSONBytes(user); + this.storage.put(keyBytes, keyBytes.length, valueBytes); + this.storage.flushWAL(); + this.userCache.invalidate(user.getUsername()); + } catch (Exception e) { + throw new AuthenticationException("update user to RocksDB failed", e); + } + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture getUser(String username) { + User user = this.userCache.get(username); + if (user == UserCacheLoader.EMPTY_USER) { + return CompletableFuture.completedFuture(null); + } + return CompletableFuture.completedFuture(user); + } + + @Override + public CompletableFuture> listUser(String filter) { + List result = new ArrayList<>(); + try (RocksIterator iterator = this.storage.iterator()) { + iterator.seekToFirst(); + while (iterator.isValid()) { + String username = new String(iterator.key(), StandardCharsets.UTF_8); + if (StringUtils.isNotBlank(filter) && !username.contains(filter)) { + iterator.next(); + continue; + } + User user = JSON.parseObject(new String(iterator.value(), StandardCharsets.UTF_8), User.class); + result.add(user); + iterator.next(); + } + } + return CompletableFuture.completedFuture(result); + } + + @Override + public void shutdown() { + if (this.storage != null) { + this.storage.shutdown(); + } + } + + private static class UserCacheLoader implements CacheLoader { + private final ConfigRocksDBStorage storage; + public static final User EMPTY_USER = new User(); + + public UserCacheLoader(ConfigRocksDBStorage storage) { + this.storage = storage; + } + + @Override + public User load(String username) { + try { + byte[] keyBytes = username.getBytes(StandardCharsets.UTF_8); + byte[] valueBytes = storage.get(keyBytes); + if (ArrayUtils.isEmpty(valueBytes)) { + return EMPTY_USER; + } + return JSON.parseObject(new String(valueBytes, StandardCharsets.UTF_8), User.class); + } catch (Exception e) { + throw new AuthenticationException("Get user from RocksDB failed.", e); + } + } + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authentication/strategy/AbstractAuthenticationStrategy.java b/auth/src/main/java/org/apache/rocketmq/auth/authentication/strategy/AbstractAuthenticationStrategy.java new file mode 100644 index 0000000..b27b6e3 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authentication/strategy/AbstractAuthenticationStrategy.java @@ -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. + */ +package org.apache.rocketmq.auth.authentication.strategy; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.auth.authentication.context.AuthenticationContext; +import org.apache.rocketmq.auth.authentication.exception.AuthenticationException; +import org.apache.rocketmq.auth.authentication.factory.AuthenticationFactory; +import org.apache.rocketmq.auth.authentication.provider.AuthenticationProvider; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.common.utils.ExceptionUtils; + +public abstract class AbstractAuthenticationStrategy implements AuthenticationStrategy { + + protected final AuthConfig authConfig; + protected final List authenticationWhitelist = new ArrayList<>(); + protected final AuthenticationProvider authenticationProvider; + + public AbstractAuthenticationStrategy(AuthConfig authConfig, Supplier metadataService) { + this.authConfig = authConfig; + this.authenticationProvider = AuthenticationFactory.getProvider(authConfig); + if (this.authenticationProvider != null) { + this.authenticationProvider.initialize(authConfig, metadataService); + } + if (StringUtils.isNotBlank(authConfig.getAuthenticationWhitelist())) { + String[] whitelist = StringUtils.split(authConfig.getAuthenticationWhitelist(), ","); + for (String rpcCode : whitelist) { + this.authenticationWhitelist.add(StringUtils.trim(rpcCode)); + } + } + } + + protected void doEvaluate(AuthenticationContext context) { + if (context == null) { + return; + } + if (!authConfig.isAuthenticationEnabled()) { + return; + } + if (this.authenticationProvider == null) { + return; + } + if (this.authenticationWhitelist.contains(context.getRpcCode())) { + return; + } + try { + this.authenticationProvider.authenticate(context).join(); + } catch (AuthenticationException ex) { + throw ex; + } catch (Throwable ex) { + Throwable exception = ExceptionUtils.getRealException(ex); + if (exception instanceof AuthenticationException) { + throw (AuthenticationException) exception; + } + throw new AuthenticationException("Authentication failed. Please verify the credentials and try again.", exception); + } + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authentication/strategy/AuthenticationStrategy.java b/auth/src/main/java/org/apache/rocketmq/auth/authentication/strategy/AuthenticationStrategy.java new file mode 100644 index 0000000..22eee6f --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authentication/strategy/AuthenticationStrategy.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.auth.authentication.strategy; + +import org.apache.rocketmq.auth.authentication.context.AuthenticationContext; + +public interface AuthenticationStrategy { + + void evaluate(AuthenticationContext context); +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authentication/strategy/StatefulAuthenticationStrategy.java b/auth/src/main/java/org/apache/rocketmq/auth/authentication/strategy/StatefulAuthenticationStrategy.java new file mode 100644 index 0000000..914d99a --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authentication/strategy/StatefulAuthenticationStrategy.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.auth.authentication.strategy; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.auth.authentication.context.AuthenticationContext; +import org.apache.rocketmq.auth.authentication.context.DefaultAuthenticationContext; +import org.apache.rocketmq.auth.authentication.exception.AuthenticationException; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.constant.CommonConstants; + +public class StatefulAuthenticationStrategy extends AbstractAuthenticationStrategy { + + protected Cache> authCache; + + public StatefulAuthenticationStrategy(AuthConfig authConfig, Supplier metadataService) { + super(authConfig, metadataService); + this.authCache = Caffeine.newBuilder() + .expireAfterWrite(authConfig.getStatefulAuthenticationCacheExpiredSecond(), TimeUnit.SECONDS) + .maximumSize(authConfig.getStatefulAuthenticationCacheMaxNum()) + .build(); + } + + @Override + public void evaluate(AuthenticationContext context) { + if (StringUtils.isBlank(context.getChannelId())) { + this.doEvaluate(context); + return; + } + Pair result = this.authCache.get(buildKey(context), key -> { + try { + this.doEvaluate(context); + return Pair.of(true, null); + } catch (AuthenticationException ex) { + return Pair.of(false, ex); + } + }); + if (result != null && result.getObject1() == Boolean.FALSE) { + throw result.getObject2(); + } + } + + private String buildKey(AuthenticationContext context) { + if (context instanceof DefaultAuthenticationContext) { + DefaultAuthenticationContext ctx = (DefaultAuthenticationContext) context; + if (StringUtils.isBlank(ctx.getUsername())) { + return ctx.getChannelId(); + } + return ctx.getChannelId() + CommonConstants.POUND + ctx.getUsername(); + } + throw new AuthenticationException("The request of {} is not support.", context.getClass().getSimpleName()); + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authentication/strategy/StatelessAuthenticationStrategy.java b/auth/src/main/java/org/apache/rocketmq/auth/authentication/strategy/StatelessAuthenticationStrategy.java new file mode 100644 index 0000000..0564982 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authentication/strategy/StatelessAuthenticationStrategy.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.auth.authentication.strategy; + +import java.util.function.Supplier; +import org.apache.rocketmq.auth.authentication.context.AuthenticationContext; +import org.apache.rocketmq.auth.config.AuthConfig; + +public class StatelessAuthenticationStrategy extends AbstractAuthenticationStrategy { + + public StatelessAuthenticationStrategy(AuthConfig authConfig, Supplier metadataService) { + super(authConfig, metadataService); + } + + @Override + public void evaluate(AuthenticationContext context) { + super.doEvaluate(context); + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/AuthorizationEvaluator.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/AuthorizationEvaluator.java new file mode 100644 index 0000000..f043810 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/AuthorizationEvaluator.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.auth.authorization; + +import java.util.List; +import java.util.function.Supplier; +import org.apache.commons.collections.CollectionUtils; +import org.apache.rocketmq.auth.authorization.context.AuthorizationContext; +import org.apache.rocketmq.auth.authorization.factory.AuthorizationFactory; +import org.apache.rocketmq.auth.authorization.strategy.AuthorizationStrategy; +import org.apache.rocketmq.auth.config.AuthConfig; + +public class AuthorizationEvaluator { + + private final AuthorizationStrategy authorizationStrategy; + + public AuthorizationEvaluator(AuthConfig authConfig) { + this(authConfig, null); + } + + public AuthorizationEvaluator(AuthConfig authConfig, Supplier metadataService) { + this.authorizationStrategy = AuthorizationFactory.getStrategy(authConfig, metadataService); + } + + public void evaluate(List contexts) { + if (CollectionUtils.isEmpty(contexts)) { + return; + } + contexts.forEach(this.authorizationStrategy::evaluate); + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/builder/AuthorizationContextBuilder.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/builder/AuthorizationContextBuilder.java new file mode 100644 index 0000000..e1e8dcc --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/builder/AuthorizationContextBuilder.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.auth.authorization.builder; + +import com.google.protobuf.GeneratedMessageV3; +import io.grpc.Metadata; +import io.netty.channel.ChannelHandlerContext; +import java.util.List; +import org.apache.rocketmq.auth.authorization.context.DefaultAuthorizationContext; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public interface AuthorizationContextBuilder { + + List build(Metadata metadata, GeneratedMessageV3 message); + + List build(ChannelHandlerContext context, RemotingCommand command); +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilder.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilder.java new file mode 100644 index 0000000..fababc0 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilder.java @@ -0,0 +1,504 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.auth.authorization.builder; + +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.NotifyClientTerminationRequest; +import apache.rocketmq.v2.QueryAssignmentRequest; +import apache.rocketmq.v2.QueryRouteRequest; +import apache.rocketmq.v2.RecallMessageRequest; +import apache.rocketmq.v2.ReceiveMessageRequest; +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 io.grpc.Metadata; +import io.netty.channel.ChannelHandlerContext; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.acl.common.AclException; +import org.apache.rocketmq.acl.common.SessionCredentials; +import org.apache.rocketmq.auth.authentication.model.Subject; +import org.apache.rocketmq.auth.authentication.model.User; +import org.apache.rocketmq.auth.authorization.context.DefaultAuthorizationContext; +import org.apache.rocketmq.auth.authorization.exception.AuthorizationException; +import org.apache.rocketmq.auth.authorization.model.Resource; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.constant.CommonConstants; +import org.apache.rocketmq.common.constant.GrpcConstants; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.resource.ResourcePattern; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.CommandCustomHeader; +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.RequestHeaderRegistry; +import org.apache.rocketmq.remoting.protocol.body.LockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.body.UnlockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumerOffsetRequestHeader; +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 DefaultAuthorizationContextBuilder implements AuthorizationContextBuilder { + + private static final String TOPIC = "topic"; + private static final String GROUP = "group"; + private static final String A = "a"; + private static final String B = "b"; + private static final String CONSUMER_GROUP = "consumerGroup"; + private final AuthConfig authConfig; + + private final RequestHeaderRegistry requestHeaderRegistry; + + public DefaultAuthorizationContextBuilder(AuthConfig authConfig) { + this.authConfig = authConfig; + this.requestHeaderRegistry = RequestHeaderRegistry.getInstance(); + } + + @Override + public List build(Metadata metadata, GeneratedMessageV3 message) { + List result = null; + if (message instanceof SendMessageRequest) { + SendMessageRequest request = (SendMessageRequest) message; + if (request.getMessagesCount() <= 0) { + throw new AuthorizationException("message is null."); + } + result = newPubContext(metadata, request.getMessages(0).getTopic()); + } + if (message instanceof RecallMessageRequest) { + RecallMessageRequest request = (RecallMessageRequest) message; + result = newPubContext(metadata, request.getTopic()); + } + if (message instanceof EndTransactionRequest) { + EndTransactionRequest request = (EndTransactionRequest) message; + result = newPubContext(metadata, request.getTopic()); + } + if (message instanceof HeartbeatRequest) { + HeartbeatRequest request = (HeartbeatRequest) message; + if (!isConsumerClientType(request.getClientType())) { + return null; + } + result = newGroupSubContexts(metadata, request.getGroup()); + } + if (message instanceof ReceiveMessageRequest) { + ReceiveMessageRequest request = (ReceiveMessageRequest) message; + if (!request.hasMessageQueue()) { + throw new AuthorizationException("messageQueue is null."); + } + result = newSubContexts(metadata, request.getGroup(), request.getMessageQueue().getTopic()); + } + if (message instanceof AckMessageRequest) { + AckMessageRequest request = (AckMessageRequest) message; + result = newSubContexts(metadata, request.getGroup(), request.getTopic()); + } + if (message instanceof ForwardMessageToDeadLetterQueueRequest) { + ForwardMessageToDeadLetterQueueRequest request = (ForwardMessageToDeadLetterQueueRequest) message; + result = newSubContexts(metadata, request.getGroup(), request.getTopic()); + } + if (message instanceof NotifyClientTerminationRequest) { + NotifyClientTerminationRequest request = (NotifyClientTerminationRequest) message; + if (StringUtils.isNotBlank(request.getGroup().getName())) { + result = newGroupSubContexts(metadata, request.getGroup()); + } + } + if (message instanceof ChangeInvisibleDurationRequest) { + ChangeInvisibleDurationRequest request = (ChangeInvisibleDurationRequest) message; + result = newGroupSubContexts(metadata, request.getGroup()); + } + if (message instanceof QueryRouteRequest) { + QueryRouteRequest request = (QueryRouteRequest) message; + result = newContext(metadata, request); + } + if (message instanceof QueryAssignmentRequest) { + QueryAssignmentRequest request = (QueryAssignmentRequest) message; + result = newSubContexts(metadata, request.getGroup(), request.getTopic()); + } + if (message instanceof TelemetryCommand) { + TelemetryCommand request = (TelemetryCommand) message; + result = newContext(metadata, request); + } + if (CollectionUtils.isNotEmpty(result)) { + result.forEach(context -> { + context.setChannelId(metadata.get(GrpcConstants.CHANNEL_ID)); + context.setRpcCode(message.getDescriptorForType().getFullName()); + }); + } + return result; + } + + @Override + public List build(ChannelHandlerContext context, RemotingCommand command) { + List result = new ArrayList<>(); + try { + HashMap fields = command.getExtFields(); + if (MapUtils.isEmpty(fields)) { + return result; + } + Subject subject = null; + if (fields.containsKey(SessionCredentials.ACCESS_KEY)) { + subject = User.of(fields.get(SessionCredentials.ACCESS_KEY)); + } + String remoteAddr = RemotingHelper.parseChannelRemoteAddr(context.channel()); + String sourceIp = StringUtils.substringBeforeLast(remoteAddr, CommonConstants.COLON); + + Resource topic; + Resource group; + switch (command.getCode()) { + case RequestCode.GET_ROUTEINFO_BY_TOPIC: + if (NamespaceUtil.isRetryTopic(fields.get(TOPIC))) { + group = Resource.ofGroup(fields.get(TOPIC)); + result.add(DefaultAuthorizationContext.of(subject, group, Arrays.asList(Action.SUB, Action.GET), sourceIp)); + } else { + topic = Resource.ofTopic(fields.get(TOPIC)); + result.add(DefaultAuthorizationContext.of(subject, topic, Arrays.asList(Action.PUB, Action.SUB, Action.GET), sourceIp)); + } + break; + case RequestCode.SEND_MESSAGE: + if (NamespaceUtil.isRetryTopic(fields.get(TOPIC))) { + if (StringUtils.isNotBlank(fields.get(GROUP))) { + group = Resource.ofGroup(fields.get(GROUP)); + } else { + group = Resource.ofGroup(fields.get(TOPIC)); + } + result.add(DefaultAuthorizationContext.of(subject, group, Action.SUB, sourceIp)); + } else { + topic = Resource.ofTopic(fields.get(TOPIC)); + result.add(DefaultAuthorizationContext.of(subject, topic, Action.PUB, sourceIp)); + } + break; + case RequestCode.SEND_MESSAGE_V2: + case RequestCode.SEND_BATCH_MESSAGE: + if (NamespaceUtil.isRetryTopic(fields.get(B))) { + if (StringUtils.isNotBlank(fields.get(A))) { + group = Resource.ofGroup(fields.get(A)); + } else { + group = Resource.ofGroup(fields.get(B)); + } + result.add(DefaultAuthorizationContext.of(subject, group, Action.SUB, sourceIp)); + } else { + topic = Resource.ofTopic(fields.get(B)); + result.add(DefaultAuthorizationContext.of(subject, topic, Action.PUB, sourceIp)); + } + break; + case RequestCode.RECALL_MESSAGE: + topic = Resource.ofTopic(fields.get(TOPIC)); + result.add(DefaultAuthorizationContext.of(subject, topic, Action.PUB, sourceIp)); + break; + case RequestCode.END_TRANSACTION: + if (StringUtils.isNotBlank(fields.get(TOPIC))) { + topic = Resource.ofTopic(fields.get(TOPIC)); + result.add(DefaultAuthorizationContext.of(subject, topic, Action.PUB, sourceIp)); + } + break; + case RequestCode.CONSUMER_SEND_MSG_BACK: + group = Resource.ofGroup(fields.get(GROUP)); + result.add(DefaultAuthorizationContext.of(subject, group, Action.SUB, sourceIp)); + break; + case RequestCode.PULL_MESSAGE: + if (!NamespaceUtil.isRetryTopic(fields.get(TOPIC))) { + topic = Resource.ofTopic(fields.get(TOPIC)); + result.add(DefaultAuthorizationContext.of(subject, topic, Action.SUB, sourceIp)); + } + group = Resource.ofGroup(fields.get(CONSUMER_GROUP)); + result.add(DefaultAuthorizationContext.of(subject, group, Action.SUB, sourceIp)); + break; + case RequestCode.QUERY_MESSAGE: + topic = Resource.ofTopic(fields.get(TOPIC)); + result.add(DefaultAuthorizationContext.of(subject, topic, Arrays.asList(Action.SUB, Action.GET), sourceIp)); + break; + case RequestCode.HEART_BEAT: + HeartbeatData heartbeatData = HeartbeatData.decode(command.getBody(), HeartbeatData.class); + for (ConsumerData data : heartbeatData.getConsumerDataSet()) { + group = Resource.ofGroup(data.getGroupName()); + result.add(DefaultAuthorizationContext.of(subject, group, Action.SUB, sourceIp)); + for (SubscriptionData subscriptionData : data.getSubscriptionDataSet()) { + if (NamespaceUtil.isRetryTopic(subscriptionData.getTopic())) { + continue; + } + topic = Resource.ofTopic(subscriptionData.getTopic()); + result.add(DefaultAuthorizationContext.of(subject, topic, Action.SUB, sourceIp)); + } + } + break; + case RequestCode.UNREGISTER_CLIENT: + final UnregisterClientRequestHeader unregisterClientRequestHeader = + command.decodeCommandCustomHeader(UnregisterClientRequestHeader.class); + if (StringUtils.isNotBlank(unregisterClientRequestHeader.getConsumerGroup())) { + group = Resource.ofGroup(unregisterClientRequestHeader.getConsumerGroup()); + result.add(DefaultAuthorizationContext.of(subject, group, Action.SUB, sourceIp)); + } + break; + case RequestCode.GET_CONSUMER_LIST_BY_GROUP: + final GetConsumerListByGroupRequestHeader getConsumerListByGroupRequestHeader = + command.decodeCommandCustomHeader(GetConsumerListByGroupRequestHeader.class); + group = Resource.ofGroup(getConsumerListByGroupRequestHeader.getConsumerGroup()); + result.add(DefaultAuthorizationContext.of(subject, group, Arrays.asList(Action.SUB, Action.GET), sourceIp)); + break; + case RequestCode.QUERY_CONSUMER_OFFSET: + final QueryConsumerOffsetRequestHeader queryConsumerOffsetRequestHeader = + command.decodeCommandCustomHeader(QueryConsumerOffsetRequestHeader.class); + if (!NamespaceUtil.isRetryTopic(queryConsumerOffsetRequestHeader.getTopic())) { + topic = Resource.ofTopic(queryConsumerOffsetRequestHeader.getTopic()); + result.add(DefaultAuthorizationContext.of(subject, topic, Arrays.asList(Action.SUB, Action.GET), sourceIp)); + } + group = Resource.ofGroup(queryConsumerOffsetRequestHeader.getConsumerGroup()); + result.add(DefaultAuthorizationContext.of(subject, group, Arrays.asList(Action.SUB, Action.GET), sourceIp)); + break; + case RequestCode.UPDATE_CONSUMER_OFFSET: + final UpdateConsumerOffsetRequestHeader updateConsumerOffsetRequestHeader = + command.decodeCommandCustomHeader(UpdateConsumerOffsetRequestHeader.class); + if (!NamespaceUtil.isRetryTopic(updateConsumerOffsetRequestHeader.getTopic())) { + topic = Resource.ofTopic(updateConsumerOffsetRequestHeader.getTopic()); + result.add(DefaultAuthorizationContext.of(subject, topic, Arrays.asList(Action.SUB, Action.UPDATE), sourceIp)); + } + group = Resource.ofGroup(updateConsumerOffsetRequestHeader.getConsumerGroup()); + result.add(DefaultAuthorizationContext.of(subject, group, Arrays.asList(Action.SUB, Action.UPDATE), sourceIp)); + break; + case RequestCode.LOCK_BATCH_MQ: + LockBatchRequestBody lockBatchRequestBody = LockBatchRequestBody.decode(command.getBody(), LockBatchRequestBody.class); + group = Resource.ofGroup(lockBatchRequestBody.getConsumerGroup()); + result.add(DefaultAuthorizationContext.of(subject, group, Action.SUB, sourceIp)); + if (CollectionUtils.isNotEmpty(lockBatchRequestBody.getMqSet())) { + for (MessageQueue messageQueue : lockBatchRequestBody.getMqSet()) { + if (NamespaceUtil.isRetryTopic(messageQueue.getTopic())) { + continue; + } + topic = Resource.ofTopic(messageQueue.getTopic()); + result.add(DefaultAuthorizationContext.of(subject, topic, Action.SUB, sourceIp)); + } + } + break; + case RequestCode.UNLOCK_BATCH_MQ: + UnlockBatchRequestBody unlockBatchRequestBody = LockBatchRequestBody.decode(command.getBody(), UnlockBatchRequestBody.class); + group = Resource.ofGroup(unlockBatchRequestBody.getConsumerGroup()); + result.add(DefaultAuthorizationContext.of(subject, group, Action.SUB, sourceIp)); + if (CollectionUtils.isNotEmpty(unlockBatchRequestBody.getMqSet())) { + for (MessageQueue messageQueue : unlockBatchRequestBody.getMqSet()) { + if (NamespaceUtil.isRetryTopic(messageQueue.getTopic())) { + continue; + } + topic = Resource.ofTopic(messageQueue.getTopic()); + result.add(DefaultAuthorizationContext.of(subject, topic, Action.SUB, sourceIp)); + } + } + break; + default: + result = buildContextByAnnotation(subject, command, sourceIp); + break; + } + if (CollectionUtils.isNotEmpty(result)) { + result.forEach(r -> { + r.setChannelId(context.channel().id().asLongText()); + r.setRpcCode(String.valueOf(command.getCode())); + }); + } + } catch (AuthorizationException ex) { + throw ex; + } catch (Throwable t) { + throw new AuthorizationException("parse authorization context error.", t); + } + return result; + } + + private List buildContextByAnnotation(Subject subject, RemotingCommand request, + String sourceIp) throws Exception { + List result = new ArrayList<>(); + + Class clazz = this.requestHeaderRegistry.getRequestHeader(request.getCode()); + if (clazz == null) { + return result; + } + CommandCustomHeader header = request.decodeCommandCustomHeader(clazz); + + RocketMQAction rocketMQAction = clazz.getAnnotation(RocketMQAction.class); + ResourceType resourceType = rocketMQAction.resource(); + Action[] actions = rocketMQAction.action(); + Resource resource = null; + if (resourceType == ResourceType.CLUSTER) { + resource = Resource.ofCluster(authConfig.getClusterName()); + } + + Field[] fields = clazz.getDeclaredFields(); + if (ArrayUtils.isNotEmpty(fields)) { + for (Field field : fields) { + RocketMQResource rocketMQResource = field.getAnnotation(RocketMQResource.class); + if (rocketMQResource == null) { + continue; + } + field.setAccessible(true); + try { + resourceType = rocketMQResource.value(); + String splitter = rocketMQResource.splitter(); + Object value = field.get(header); + if (value == null) { + continue; + } + String[] resourceValues; + if (StringUtils.isNotBlank(splitter)) { + resourceValues = StringUtils.split(value.toString(), splitter); + } else { + resourceValues = new String[] {value.toString()}; + } + for (String resourceValue : resourceValues) { + if (resourceType == ResourceType.TOPIC && NamespaceUtil.isRetryTopic(resourceValue)) { + resource = Resource.ofGroup(resourceValue); + result.add(DefaultAuthorizationContext.of(subject, resource, Arrays.asList(actions), sourceIp)); + } else { + resource = Resource.of(resourceType, resourceValue, ResourcePattern.LITERAL); + result.add(DefaultAuthorizationContext.of(subject, resource, Arrays.asList(actions), sourceIp)); + } + } + } finally { + field.setAccessible(false); + } + } + } + + if (CollectionUtils.isEmpty(result) && resource != null) { + result.add(DefaultAuthorizationContext.of(subject, resource, Arrays.asList(actions), sourceIp)); + } + + return result; + } + + private List newContext(Metadata metadata, QueryRouteRequest request) { + apache.rocketmq.v2.Resource topic = request.getTopic(); + if (StringUtils.isBlank(topic.getName())) { + throw new AuthorizationException("topic is null."); + } + Subject subject = null; + if (metadata.containsKey(GrpcConstants.AUTHORIZATION_AK)) { + subject = User.of(metadata.get(GrpcConstants.AUTHORIZATION_AK)); + } + Resource resource = Resource.ofTopic(topic.getName()); + String sourceIp = StringUtils.substringBeforeLast(metadata.get(GrpcConstants.REMOTE_ADDRESS), CommonConstants.COLON); + DefaultAuthorizationContext context = DefaultAuthorizationContext.of(subject, resource, Arrays.asList(Action.PUB, Action.SUB), sourceIp); + return Collections.singletonList(context); + } + + private static List newContext(Metadata metadata, TelemetryCommand request) { + if (request.getCommandCase() != TelemetryCommand.CommandCase.SETTINGS) { + return null; + } + if (!request.getSettings().hasPublishing() && !request.getSettings().hasSubscription()) { + throw new AclException("settings command doesn't have publishing or subscription."); + } + List result = new ArrayList<>(); + if (request.getSettings().hasPublishing()) { + List topicList = request.getSettings().getPublishing().getTopicsList(); + for (apache.rocketmq.v2.Resource topic : topicList) { + result.addAll(newPubContext(metadata, topic)); + } + } + if (request.getSettings().hasSubscription()) { + Subscription subscription = request.getSettings().getSubscription(); + result.addAll(newSubContexts(metadata, ResourceType.GROUP, subscription.getGroup())); + for (SubscriptionEntry entry : subscription.getSubscriptionsList()) { + result.addAll(newSubContexts(metadata, ResourceType.TOPIC, entry.getTopic())); + } + } + return result; + } + + private boolean isConsumerClientType(ClientType clientType) { + return Arrays.asList(ClientType.PUSH_CONSUMER, ClientType.SIMPLE_CONSUMER, ClientType.PULL_CONSUMER) + .contains(clientType); + } + + private static List newPubContext(Metadata metadata, apache.rocketmq.v2.Resource topic) { + if (topic == null || StringUtils.isBlank(topic.getName())) { + throw new AuthorizationException("topic is null."); + } + Subject subject = null; + if (metadata.containsKey(GrpcConstants.AUTHORIZATION_AK)) { + subject = User.of(metadata.get(GrpcConstants.AUTHORIZATION_AK)); + } + Resource resource = Resource.ofTopic(topic.getName()); + String sourceIp = StringUtils.substringBeforeLast(metadata.get(GrpcConstants.REMOTE_ADDRESS), CommonConstants.COLON); + DefaultAuthorizationContext context = DefaultAuthorizationContext.of(subject, resource, Action.PUB, sourceIp); + return Collections.singletonList(context); + } + + private List newSubContexts(Metadata metadata, apache.rocketmq.v2.Resource group, + apache.rocketmq.v2.Resource topic) { + List result = new ArrayList<>(); + result.addAll(newGroupSubContexts(metadata, group)); + result.addAll(newTopicSubContexts(metadata, topic)); + return result; + } + + private static List newTopicSubContexts(Metadata metadata, + apache.rocketmq.v2.Resource resource) { + return newSubContexts(metadata, ResourceType.TOPIC, resource); + } + + private static List newGroupSubContexts(Metadata metadata, + apache.rocketmq.v2.Resource resource) { + return newSubContexts(metadata, ResourceType.GROUP, resource); + } + + private static List newSubContexts(Metadata metadata, ResourceType resourceType, + apache.rocketmq.v2.Resource resource) { + if (resourceType == ResourceType.GROUP) { + if (resource == null || StringUtils.isBlank(resource.getName())) { + throw new AuthorizationException("group is null."); + } + return newSubContexts(metadata, Resource.ofGroup(resource.getName())); + } + if (resourceType == ResourceType.TOPIC) { + if (resource == null || StringUtils.isBlank(resource.getName())) { + throw new AuthorizationException("topic is null."); + } + return newSubContexts(metadata, Resource.ofTopic(resource.getName())); + } + throw new AuthorizationException("unknown resource type."); + } + + private static List newSubContexts(Metadata metadata, Resource resource) { + List result = new ArrayList<>(); + Subject subject = null; + if (metadata.containsKey(GrpcConstants.AUTHORIZATION_AK)) { + subject = User.of(metadata.get(GrpcConstants.AUTHORIZATION_AK)); + } + String sourceIp = StringUtils.substringBeforeLast(metadata.get(GrpcConstants.REMOTE_ADDRESS), CommonConstants.COLON); + result.add(DefaultAuthorizationContext.of(subject, resource, Action.SUB, sourceIp)); + return result; + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/chain/AclAuthorizationHandler.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/chain/AclAuthorizationHandler.java new file mode 100644 index 0000000..06a130b --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/chain/AclAuthorizationHandler.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.auth.authorization.chain; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.apache.commons.collections.CollectionUtils; +import org.apache.rocketmq.auth.authorization.context.DefaultAuthorizationContext; +import org.apache.rocketmq.auth.authorization.enums.Decision; +import org.apache.rocketmq.auth.authorization.enums.PolicyType; +import org.apache.rocketmq.auth.authorization.exception.AuthorizationException; +import org.apache.rocketmq.auth.authorization.factory.AuthorizationFactory; +import org.apache.rocketmq.auth.authorization.model.Acl; +import org.apache.rocketmq.auth.authorization.model.Environment; +import org.apache.rocketmq.auth.authorization.model.Policy; +import org.apache.rocketmq.auth.authorization.model.PolicyEntry; +import org.apache.rocketmq.auth.authorization.model.Resource; +import org.apache.rocketmq.auth.authorization.provider.AuthorizationMetadataProvider; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.common.chain.Handler; +import org.apache.rocketmq.common.chain.HandlerChain; +import org.apache.rocketmq.common.resource.ResourcePattern; +import org.apache.rocketmq.common.resource.ResourceType; + +public class AclAuthorizationHandler implements Handler> { + + private final AuthorizationMetadataProvider authorizationMetadataProvider; + + public AclAuthorizationHandler(AuthConfig config) { + this.authorizationMetadataProvider = AuthorizationFactory.getMetadataProvider(config); + } + + public AclAuthorizationHandler(AuthConfig config, Supplier metadataService) { + this.authorizationMetadataProvider = AuthorizationFactory.getMetadataProvider(config, metadataService); + } + + @Override + public CompletableFuture handle(DefaultAuthorizationContext context, + HandlerChain> chain) { + if (this.authorizationMetadataProvider == null) { + throw new AuthorizationException("The authorizationMetadataProvider is not configured"); + } + return this.authorizationMetadataProvider.getAcl(context.getSubject()).thenAccept(acl -> { + if (acl == null) { + throwException(context, "no matched policies."); + } + + // 1. get the defined acl entries which match the request. + PolicyEntry matchedEntry = matchPolicyEntries(context, acl); + + // 2. if no matched acl entries, return deny + if (matchedEntry == null) { + throwException(context, "no matched policies."); + } + + // 3. judge is the entries has denied decision. + if (matchedEntry.getDecision() == Decision.DENY) { + throwException(context, "the decision is deny."); + } + }); + } + + private PolicyEntry matchPolicyEntries(DefaultAuthorizationContext context, Acl acl) { + List policyEntries = new ArrayList<>(); + + Policy policy = acl.getPolicy(PolicyType.CUSTOM); + if (policy != null) { + List entries = matchPolicyEntries(context, policy.getEntries()); + if (CollectionUtils.isNotEmpty(entries)) { + policyEntries.addAll(entries); + } + } + + if (CollectionUtils.isEmpty(policyEntries)) { + policy = acl.getPolicy(PolicyType.DEFAULT); + if (policy != null) { + List entries = matchPolicyEntries(context, policy.getEntries()); + if (CollectionUtils.isNotEmpty(entries)) { + policyEntries.addAll(entries); + } + } + } + + if (CollectionUtils.isEmpty(policyEntries)) { + return null; + } + + policyEntries.sort(this::comparePolicyEntries); + + return policyEntries.get(0); + } + + private List matchPolicyEntries(DefaultAuthorizationContext context, List entries) { + if (CollectionUtils.isEmpty(entries)) { + return null; + } + return entries.stream() + .filter(entry -> entry.isMatchResource(context.getResource())) + .filter(entry -> entry.isMatchAction(context.getActions())) + .filter(entry -> entry.isMatchEnvironment(Environment.of(context.getSourceIp()))) + .collect(Collectors.toList()); + } + + private int comparePolicyEntries(PolicyEntry o1, PolicyEntry o2) { + int compare = 0; + Resource r1 = o1.getResource(); + Resource r2 = o2.getResource(); + if (r1.getResourceType() != r2.getResourceType()) { + if (r1.getResourceType() == ResourceType.ANY) { + compare = 1; + } + if (r2.getResourceType() == ResourceType.ANY) { + compare = -1; + } + } else if (r1.getResourcePattern() == r2.getResourcePattern()) { + if (r1.getResourcePattern() == ResourcePattern.PREFIXED) { + String n1 = r1.getResourceName(); + String n2 = r2.getResourceName(); + compare = Integer.compare(n1.length(), n2.length()); + } + } else { + if (r1.getResourcePattern() == ResourcePattern.LITERAL) { + compare = 1; + } + if (r1.getResourcePattern() == ResourcePattern.LITERAL) { + compare = -1; + } + if (r1.getResourcePattern() == ResourcePattern.PREFIXED) { + compare = 1; + } + if (r1.getResourcePattern() == ResourcePattern.PREFIXED) { + compare = -1; + } + } + + if (compare != 0) { + return compare; + } + + // the decision deny has higher priority + Decision d1 = o1.getDecision(); + Decision d2 = o2.getDecision(); + return d1 == Decision.DENY ? 1 : d2 == Decision.DENY ? -1 : 0; + } + + private static void throwException(DefaultAuthorizationContext context, String detail) { + throw new AuthorizationException("{} has no permission to access {} from {}, " + detail, + context.getSubject().getSubjectKey(), context.getResource().getResourceKey(), context.getSourceIp()); + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/chain/UserAuthorizationHandler.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/chain/UserAuthorizationHandler.java new file mode 100644 index 0000000..1c391df --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/chain/UserAuthorizationHandler.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.auth.authorization.chain; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import org.apache.rocketmq.auth.authentication.enums.SubjectType; +import org.apache.rocketmq.auth.authentication.enums.UserStatus; +import org.apache.rocketmq.auth.authentication.enums.UserType; +import org.apache.rocketmq.auth.authentication.exception.AuthenticationException; +import org.apache.rocketmq.auth.authentication.factory.AuthenticationFactory; +import org.apache.rocketmq.auth.authentication.model.Subject; +import org.apache.rocketmq.auth.authentication.model.User; +import org.apache.rocketmq.auth.authentication.provider.AuthenticationMetadataProvider; +import org.apache.rocketmq.auth.authorization.context.DefaultAuthorizationContext; +import org.apache.rocketmq.auth.authorization.exception.AuthorizationException; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.common.chain.Handler; +import org.apache.rocketmq.common.chain.HandlerChain; + +public class UserAuthorizationHandler implements Handler> { + + private final AuthenticationMetadataProvider authenticationMetadataProvider; + + public UserAuthorizationHandler(AuthConfig config, Supplier metadataService) { + this.authenticationMetadataProvider = AuthenticationFactory.getMetadataProvider(config, metadataService); + } + + @Override + public CompletableFuture handle(DefaultAuthorizationContext context, HandlerChain> chain) { + if (!context.getSubject().isSubject(SubjectType.USER)) { + return chain.handle(context); + } + return this.getUser(context.getSubject()).thenCompose(user -> { + if (user.getUserType() == UserType.SUPER) { + return CompletableFuture.completedFuture(null); + } + return chain.handle(context); + }); + } + + private CompletableFuture getUser(Subject subject) { + if (this.authenticationMetadataProvider == null) { + throw new AuthorizationException("The authenticationMetadataProvider is not configured"); + } + User user = (User) subject; + return authenticationMetadataProvider.getUser(user.getUsername()).thenApply(result -> { + if (result == null) { + throw new AuthorizationException("User:{} not found.", user.getUsername()); + } + if (user.getUserStatus() == UserStatus.DISABLE) { + throw new AuthenticationException("User:{} is disabled.", user.getUsername()); + } + return result; + }); + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/context/AuthorizationContext.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/context/AuthorizationContext.java new file mode 100644 index 0000000..d2286d7 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/context/AuthorizationContext.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.auth.authorization.context; + +import java.util.HashMap; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; + +public abstract class AuthorizationContext { + + private String channelId; + + private String rpcCode; + + private Map extInfo; + + @SuppressWarnings("unchecked") + public T getExtInfo(String key) { + if (StringUtils.isBlank(key)) { + return null; + } + if (this.extInfo == null) { + return null; + } + Object value = this.extInfo.get(key); + if (value == null) { + return null; + } + return (T) value; + } + + public void setExtInfo(String key, Object value) { + if (StringUtils.isBlank(key) || value == null) { + return; + } + if (this.extInfo == null) { + this.extInfo = new HashMap<>(); + } + this.extInfo.put(key, value); + } + + public boolean hasExtInfo(String key) { + Object value = getExtInfo(key); + return value != null; + } + + public String getChannelId() { + return channelId; + } + + public void setChannelId(String channelId) { + this.channelId = channelId; + } + + public String getRpcCode() { + return rpcCode; + } + + public void setRpcCode(String rpcCode) { + this.rpcCode = rpcCode; + } + + public Map getExtInfo() { + return extInfo; + } + + public void setExtInfo(Map extInfo) { + this.extInfo = extInfo; + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/context/DefaultAuthorizationContext.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/context/DefaultAuthorizationContext.java new file mode 100644 index 0000000..8e38ed2 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/context/DefaultAuthorizationContext.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.auth.authorization.context; + +import java.util.Collections; +import java.util.List; +import org.apache.rocketmq.auth.authentication.model.Subject; +import org.apache.rocketmq.auth.authorization.model.Resource; +import org.apache.rocketmq.common.action.Action; + +public class DefaultAuthorizationContext extends AuthorizationContext { + + private Subject subject; + + private Resource resource; + + private List actions; + + private String sourceIp; + + public static DefaultAuthorizationContext of(Subject subject, Resource resource, Action action, String sourceIp) { + DefaultAuthorizationContext context = new DefaultAuthorizationContext(); + context.setSubject(subject); + context.setResource(resource); + context.setActions(Collections.singletonList(action)); + context.setSourceIp(sourceIp); + return context; + } + + public static DefaultAuthorizationContext of(Subject subject, Resource resource, List actions, String sourceIp) { + DefaultAuthorizationContext context = new DefaultAuthorizationContext(); + context.setSubject(subject); + context.setResource(resource); + context.setActions(actions); + context.setSourceIp(sourceIp); + return context; + } + + public String getSubjectKey() { + return this.subject != null ? this.subject.getSubjectKey() : null; + } + + public String getResourceKey() { + return this.resource != null ? this.resource.getResourceKey() : null; + } + + public Subject getSubject() { + return subject; + } + + public void setSubject(Subject subject) { + this.subject = subject; + } + + public Resource getResource() { + return resource; + } + + public void setResource(Resource resource) { + this.resource = resource; + } + + public List getActions() { + return actions; + } + + public void setActions(List actions) { + this.actions = actions; + } + + public String getSourceIp() { + return sourceIp; + } + + public void setSourceIp(String sourceIp) { + this.sourceIp = sourceIp; + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/enums/Decision.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/enums/Decision.java new file mode 100644 index 0000000..b7e69b7 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/enums/Decision.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.auth.authorization.enums; + +import com.alibaba.fastjson2.annotation.JSONField; +import org.apache.commons.lang3.StringUtils; + +public enum Decision { + + ALLOW((byte) 1, "Allow"), + + DENY((byte) 2, "Deny"); + + @JSONField(value = true) + private final byte code; + private final String name; + + Decision(byte code, String name) { + this.code = code; + this.name = name; + } + + public static Decision getByName(String name) { + for (Decision decision : Decision.values()) { + if (StringUtils.equalsIgnoreCase(decision.getName(), name)) { + return decision; + } + } + return null; + } + + public byte getCode() { + return code; + } + + public String getName() { + return name; + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/enums/PolicyType.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/enums/PolicyType.java new file mode 100644 index 0000000..fff71d6 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/enums/PolicyType.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.auth.authorization.enums; + +import com.alibaba.fastjson2.annotation.JSONField; +import org.apache.commons.lang3.StringUtils; + +public enum PolicyType { + + CUSTOM((byte) 1, "Custom"), + + DEFAULT((byte) 2, "Default"); + + @JSONField(value = true) + private final byte code; + private final String name; + + PolicyType(byte code, String name) { + this.code = code; + this.name = name; + } + + public static PolicyType getByName(String name) { + for (PolicyType policyType : PolicyType.values()) { + if (StringUtils.equalsIgnoreCase(policyType.getName(), name)) { + return policyType; + } + } + return null; + } + + public byte getCode() { + return code; + } + + public String getName() { + return name; + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/exception/AuthorizationException.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/exception/AuthorizationException.java new file mode 100644 index 0000000..e2aadcb --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/exception/AuthorizationException.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.auth.authorization.exception; + +import org.slf4j.helpers.MessageFormatter; + +public class AuthorizationException extends RuntimeException { + + public AuthorizationException(String message) { + super(message); + } + + public AuthorizationException(String message, Throwable cause) { + super(message, cause); + } + + public AuthorizationException(String messagePattern, Object... argArray) { + super(MessageFormatter.arrayFormat(messagePattern, argArray).getMessage()); + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/factory/AuthorizationFactory.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/factory/AuthorizationFactory.java new file mode 100644 index 0000000..29748a9 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/factory/AuthorizationFactory.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.auth.authorization.factory; + +import com.google.protobuf.GeneratedMessageV3; +import io.grpc.Metadata; +import io.netty.channel.ChannelHandlerContext; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.auth.authorization.AuthorizationEvaluator; +import org.apache.rocketmq.auth.authorization.context.AuthorizationContext; +import org.apache.rocketmq.auth.authorization.manager.AuthorizationMetadataManager; +import org.apache.rocketmq.auth.authorization.manager.AuthorizationMetadataManagerImpl; +import org.apache.rocketmq.auth.authorization.provider.AuthorizationMetadataProvider; +import org.apache.rocketmq.auth.authorization.provider.AuthorizationProvider; +import org.apache.rocketmq.auth.authorization.provider.DefaultAuthorizationProvider; +import org.apache.rocketmq.auth.authorization.strategy.AuthorizationStrategy; +import org.apache.rocketmq.auth.authorization.strategy.StatelessAuthorizationStrategy; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public class AuthorizationFactory { + + private static final Map INSTANCE_MAP = new HashMap<>(); + private static final String PROVIDER_PREFIX = "PROVIDER_"; + private static final String METADATA_PROVIDER_PREFIX = "METADATA_PROVIDER_"; + private static final String EVALUATOR_PREFIX = "EVALUATOR_"; + + @SuppressWarnings("unchecked") + public static AuthorizationProvider getProvider(AuthConfig config) { + if (config == null) { + return null; + } + return computeIfAbsent(PROVIDER_PREFIX + config.getConfigName(), key -> { + try { + Class> clazz = + DefaultAuthorizationProvider.class; + if (StringUtils.isNotBlank(config.getAuthorizationProvider())) { + clazz = (Class>) Class.forName(config.getAuthorizationProvider()); + } + return (AuthorizationProvider) clazz + .getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException("Failed to load the authorization provider.", e); + } + }); + } + + public static AuthorizationMetadataProvider getMetadataProvider(AuthConfig config) { + return getMetadataProvider(config, null); + } + + public static AuthorizationMetadataManager getMetadataManager(AuthConfig config) { + return new AuthorizationMetadataManagerImpl(config); + } + + @SuppressWarnings("unchecked") + public static AuthorizationMetadataProvider getMetadataProvider(AuthConfig config, Supplier metadataService) { + if (config == null) { + return null; + } + return computeIfAbsent(METADATA_PROVIDER_PREFIX + config.getConfigName(), key -> { + try { + if (StringUtils.isBlank(config.getAuthorizationMetadataProvider())) { + return null; + } + Class clazz = (Class) + Class.forName(config.getAuthorizationMetadataProvider()); + AuthorizationMetadataProvider result = clazz.getDeclaredConstructor().newInstance(); + result.initialize(config, metadataService); + return result; + } catch (Exception e) { + throw new RuntimeException("Failed to load the authorization metadata provider.", e); + } + }); + } + + public static AuthorizationEvaluator getEvaluator(AuthConfig config) { + return computeIfAbsent(EVALUATOR_PREFIX + config.getConfigName(), key -> new AuthorizationEvaluator(config)); + } + + public static AuthorizationEvaluator getEvaluator(AuthConfig config, Supplier metadataService) { + return computeIfAbsent(EVALUATOR_PREFIX + config.getConfigName(), key -> new AuthorizationEvaluator(config, metadataService)); + } + + @SuppressWarnings("unchecked") + public static AuthorizationStrategy getStrategy(AuthConfig config, Supplier metadataService) { + try { + Class clazz = StatelessAuthorizationStrategy.class; + if (StringUtils.isNotBlank(config.getAuthorizationStrategy())) { + clazz = (Class) Class.forName(config.getAuthorizationStrategy()); + } + return clazz.getDeclaredConstructor(AuthConfig.class, Supplier.class).newInstance(config, metadataService); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static List newContexts(AuthConfig config, Metadata metadata, + GeneratedMessageV3 message) { + AuthorizationProvider authorizationProvider = getProvider(config); + if (authorizationProvider == null) { + return null; + } + return authorizationProvider.newContexts(metadata, message); + } + + public static List newContexts(AuthConfig config, ChannelHandlerContext context, + RemotingCommand command) { + AuthorizationProvider authorizationProvider = getProvider(config); + if (authorizationProvider == null) { + return null; + } + return authorizationProvider.newContexts(context, command); + } + + @SuppressWarnings("unchecked") + private static V computeIfAbsent(String key, Function function) { + Object result = null; + if (INSTANCE_MAP.containsKey(key)) { + result = INSTANCE_MAP.get(key); + } + if (result == null) { + synchronized (INSTANCE_MAP) { + if (INSTANCE_MAP.containsKey(key)) { + result = INSTANCE_MAP.get(key); + } + if (result == null) { + result = function.apply(key); + if (result != null) { + INSTANCE_MAP.put(key, result); + } + } + } + } + return result != null ? (V) result : null; + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/manager/AuthorizationMetadataManager.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/manager/AuthorizationMetadataManager.java new file mode 100644 index 0000000..ce96230 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/manager/AuthorizationMetadataManager.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.auth.authorization.manager; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.auth.authentication.model.Subject; +import org.apache.rocketmq.auth.authorization.enums.PolicyType; +import org.apache.rocketmq.auth.authorization.model.Acl; +import org.apache.rocketmq.auth.authorization.model.Resource; + +public interface AuthorizationMetadataManager { + + void shutdown(); + + CompletableFuture createAcl(Acl acl); + + CompletableFuture updateAcl(Acl acl); + + CompletableFuture deleteAcl(Subject subject); + + CompletableFuture deleteAcl(Subject subject, PolicyType policyType, Resource resource); + + CompletableFuture getAcl(Subject subject); + + CompletableFuture> listAcl(String subjectFilter, String resourceFilter); +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/manager/AuthorizationMetadataManagerImpl.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/manager/AuthorizationMetadataManagerImpl.java new file mode 100644 index 0000000..52b62f7 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/manager/AuthorizationMetadataManagerImpl.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.auth.authorization.manager; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.auth.authentication.enums.SubjectType; +import org.apache.rocketmq.auth.authentication.factory.AuthenticationFactory; +import org.apache.rocketmq.auth.authentication.model.Subject; +import org.apache.rocketmq.auth.authentication.model.User; +import org.apache.rocketmq.auth.authentication.provider.AuthenticationMetadataProvider; +import org.apache.rocketmq.auth.authorization.enums.PolicyType; +import org.apache.rocketmq.auth.authorization.exception.AuthorizationException; +import org.apache.rocketmq.auth.authorization.factory.AuthorizationFactory; +import org.apache.rocketmq.auth.authorization.model.Acl; +import org.apache.rocketmq.auth.authorization.model.Environment; +import org.apache.rocketmq.auth.authorization.model.Policy; +import org.apache.rocketmq.auth.authorization.model.PolicyEntry; +import org.apache.rocketmq.auth.authorization.model.Resource; +import org.apache.rocketmq.auth.authorization.provider.AuthorizationMetadataProvider; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.utils.ExceptionUtils; +import org.apache.rocketmq.common.utils.IPAddressUtils; + +public class AuthorizationMetadataManagerImpl implements AuthorizationMetadataManager { + + private final AuthorizationMetadataProvider authorizationMetadataProvider; + + private final AuthenticationMetadataProvider authenticationMetadataProvider; + + public AuthorizationMetadataManagerImpl(AuthConfig authConfig) { + this.authorizationMetadataProvider = AuthorizationFactory.getMetadataProvider(authConfig); + this.authenticationMetadataProvider = AuthenticationFactory.getMetadataProvider(authConfig); + } + + @Override + public void shutdown() { + if (this.authenticationMetadataProvider != null) { + this.authenticationMetadataProvider.shutdown(); + } + if (this.authorizationMetadataProvider != null) { + this.authorizationMetadataProvider.shutdown(); + } + } + + @Override + public CompletableFuture createAcl(Acl acl) { + try { + validate(acl); + + initAcl(acl); + + CompletableFuture subjectFuture; + if (acl.getSubject().isSubject(SubjectType.USER)) { + User user = (User) acl.getSubject(); + subjectFuture = this.getAuthenticationMetadataProvider().getUser(user.getUsername()); + } else { + subjectFuture = CompletableFuture.completedFuture(acl.getSubject()); + } + + return subjectFuture.thenCompose(subject -> { + if (subject == null) { + throw new AuthorizationException("The subject of {} is not exist.", acl.getSubject().getSubjectKey()); + } + return this.getAuthorizationMetadataProvider().getAcl(acl.getSubject()); + }).thenCompose(oldAcl -> { + if (oldAcl == null) { + return this.getAuthorizationMetadataProvider().createAcl(acl); + } + oldAcl.updatePolicy(acl.getPolicies()); + return this.getAuthorizationMetadataProvider().updateAcl(oldAcl); + }); + + } catch (Exception e) { + return this.handleException(e); + } + } + + @Override + public CompletableFuture updateAcl(Acl acl) { + try { + validate(acl); + + initAcl(acl); + + CompletableFuture subjectFuture; + if (acl.getSubject().isSubject(SubjectType.USER)) { + User user = (User) acl.getSubject(); + subjectFuture = this.getAuthenticationMetadataProvider().getUser(user.getUsername()); + } else { + subjectFuture = CompletableFuture.completedFuture(acl.getSubject()); + } + + return subjectFuture.thenCompose(subject -> { + if (subject == null) { + throw new AuthorizationException("The subject of {} is not exist.", acl.getSubject().getSubjectKey()); + } + return this.getAuthorizationMetadataProvider().getAcl(acl.getSubject()); + }).thenCompose(oldAcl -> { + if (oldAcl == null) { + return this.getAuthorizationMetadataProvider().createAcl(acl); + } + oldAcl.updatePolicy(acl.getPolicies()); + return this.getAuthorizationMetadataProvider().updateAcl(oldAcl); + }); + + } catch (Exception e) { + return this.handleException(e); + } + } + + @Override + public CompletableFuture deleteAcl(Subject subject) { + return this.deleteAcl(subject, null, null); + } + + @Override + public CompletableFuture deleteAcl(Subject subject, PolicyType policyType, Resource resource) { + try { + if (subject == null) { + throw new AuthorizationException("The subject is null."); + } + if (policyType == null) { + policyType = PolicyType.CUSTOM; + } + + CompletableFuture subjectFuture; + if (subject.isSubject(SubjectType.USER)) { + User user = (User) subject; + subjectFuture = this.getAuthenticationMetadataProvider().getUser(user.getUsername()); + } else { + subjectFuture = CompletableFuture.completedFuture(subject); + } + CompletableFuture aclFuture = this.getAuthorizationMetadataProvider().getAcl(subject); + + PolicyType finalPolicyType = policyType; + return subjectFuture.thenCombine(aclFuture, (sub, oldAcl) -> { + if (sub == null) { + throw new AuthorizationException("The subject is not exist."); + } + if (oldAcl == null) { + throw new AuthorizationException("The acl is not exist."); + } + return oldAcl; + }).thenCompose(oldAcl -> { + if (resource != null) { + oldAcl.deletePolicy(finalPolicyType, resource); + } + if (resource == null || CollectionUtils.isEmpty(oldAcl.getPolicies())) { + return this.getAuthorizationMetadataProvider().deleteAcl(subject); + } + return this.getAuthorizationMetadataProvider().updateAcl(oldAcl); + }); + + } catch (Exception e) { + return this.handleException(e); + } + } + + @Override + public CompletableFuture getAcl(Subject subject) { + CompletableFuture subjectFuture; + if (subject.isSubject(SubjectType.USER)) { + User user = (User) subject; + subjectFuture = this.getAuthenticationMetadataProvider().getUser(user.getUsername()); + } else { + subjectFuture = CompletableFuture.completedFuture(subject); + } + return subjectFuture.thenCompose(sub -> { + if (sub == null) { + throw new AuthorizationException("The subject is not exist."); + } + return this.getAuthorizationMetadataProvider().getAcl(subject); + }); + } + + @Override + public CompletableFuture> listAcl(String subjectFilter, String resourceFilter) { + return this.getAuthorizationMetadataProvider().listAcl(subjectFilter, resourceFilter); + } + + private static void initAcl(Acl acl) { + acl.getPolicies().forEach(policy -> { + if (policy.getPolicyType() == null) { + policy.setPolicyType(PolicyType.CUSTOM); + } + }); + } + + private void validate(Acl acl) { + Subject subject = acl.getSubject(); + if (subject.getSubjectType() == null) { + throw new AuthorizationException("The subject type is null."); + } + List policies = acl.getPolicies(); + if (CollectionUtils.isEmpty(policies)) { + throw new AuthorizationException("The policies is empty."); + } + for (Policy policy : policies) { + this.validate(policy); + } + } + + private void validate(Policy policy) { + List policyEntries = policy.getEntries(); + if (CollectionUtils.isEmpty(policyEntries)) { + throw new AuthorizationException("The policy entries is empty."); + } + for (PolicyEntry policyEntry : policyEntries) { + this.validate(policyEntry); + } + } + + private void validate(PolicyEntry entry) { + Resource resource = entry.getResource(); + if (resource == null) { + throw new AuthorizationException("The resource is null."); + } + if (resource.getResourceType() == null) { + throw new AuthorizationException("The resource type is null."); + } + if (resource.getResourcePattern() == null) { + throw new AuthorizationException("The resource pattern is null."); + } + if (CollectionUtils.isEmpty(entry.getActions())) { + throw new AuthorizationException("The actions is empty."); + } + if (entry.getActions().contains(Action.ANY)) { + throw new AuthorizationException("The actions can not be Any."); + } + Environment environment = entry.getEnvironment(); + if (environment != null && CollectionUtils.isNotEmpty(environment.getSourceIps())) { + for (String sourceIp : environment.getSourceIps()) { + if (StringUtils.isBlank(sourceIp)) { + throw new AuthorizationException("The source ip is empty."); + } + if (!IPAddressUtils.isValidIPOrCidr(sourceIp)) { + throw new AuthorizationException("The source ip is invalid."); + } + } + } + if (entry.getDecision() == null) { + throw new AuthorizationException("The decision is null or illegal."); + } + } + + private CompletableFuture handleException(Exception e) { + CompletableFuture result = new CompletableFuture<>(); + Throwable throwable = ExceptionUtils.getRealException(e); + result.completeExceptionally(throwable); + return result; + } + + private AuthenticationMetadataProvider getAuthenticationMetadataProvider() { + if (authorizationMetadataProvider == null) { + throw new IllegalStateException("The authenticationMetadataProvider is not configured."); + } + return authenticationMetadataProvider; + } + + private AuthorizationMetadataProvider getAuthorizationMetadataProvider() { + if (authenticationMetadataProvider == null) { + throw new IllegalStateException("The authenticationMetadataProvider is not configured."); + } + return authorizationMetadataProvider; + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/model/Acl.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/model/Acl.java new file mode 100644 index 0000000..75d34b0 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/model/Acl.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.auth.authorization.model; + +import com.google.common.collect.Lists; +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.collections.CollectionUtils; +import org.apache.rocketmq.auth.authentication.model.Subject; +import org.apache.rocketmq.auth.authorization.enums.Decision; +import org.apache.rocketmq.auth.authorization.enums.PolicyType; +import org.apache.rocketmq.common.action.Action; + +public class Acl { + + private Subject subject; + + private List policies; + + public static Acl of(Subject subject, Policy policy) { + return of(subject, Lists.newArrayList(policy)); + } + + public static Acl of(Subject subject, List policies) { + Acl acl = new Acl(); + acl.setSubject(subject); + acl.setPolicies(policies); + return acl; + } + + public static Acl of(Subject subject, List resources, List actions, Environment environment, + Decision decision) { + Acl acl = new Acl(); + acl.setSubject(subject); + Policy policy = Policy.of(resources, actions, environment, decision); + acl.setPolicies(Lists.newArrayList(policy)); + return acl; + } + + public void updatePolicy(Policy policy) { + this.updatePolicy(Lists.newArrayList(policy)); + } + + public void updatePolicy(List policies) { + if (this.policies == null) { + this.policies = new ArrayList<>(); + } + policies.forEach(newPolicy -> { + Policy oldPolicy = this.getPolicy(newPolicy.getPolicyType()); + if (oldPolicy == null) { + this.policies.add(newPolicy); + } else { + oldPolicy.updateEntry(newPolicy.getEntries()); + } + }); + } + + public void deletePolicy(PolicyType policyType, Resource resource) { + Policy policy = getPolicy(policyType); + if (policy == null) { + return; + } + policy.deleteEntry(resource); + if (CollectionUtils.isEmpty(policy.getEntries())) { + this.policies.remove(policy); + } + } + + public Policy getPolicy(PolicyType policyType) { + if (CollectionUtils.isEmpty(this.policies)) { + return null; + } + for (Policy policy : this.policies) { + if (policy.getPolicyType() == policyType) { + return policy; + } + } + return null; + } + + public Subject getSubject() { + return subject; + } + + public void setSubject(Subject subject) { + this.subject = subject; + } + + public List getPolicies() { + return policies; + } + + public void setPolicies(List policies) { + this.policies = policies; + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/model/Environment.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/model/Environment.java new file mode 100644 index 0000000..f514e20 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/model/Environment.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.auth.authorization.model; + +import java.util.Collections; +import java.util.List; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.utils.IPAddressUtils; + +public class Environment { + + private List sourceIps; + + public static Environment of(String sourceIp) { + if (StringUtils.isEmpty(sourceIp)) { + return null; + } + return of(Collections.singletonList(sourceIp)); + } + + public static Environment of(List sourceIps) { + if (CollectionUtils.isEmpty(sourceIps)) { + return null; + } + Environment environment = new Environment(); + environment.setSourceIps(sourceIps); + return environment; + } + + public boolean isMatch(Environment environment) { + if (CollectionUtils.isEmpty(this.sourceIps)) { + return true; + } + if (CollectionUtils.isEmpty(environment.getSourceIps())) { + return false; + } + String targetIp = environment.getSourceIps().get(0); + for (String sourceIp : this.sourceIps) { + if (IPAddressUtils.isIPInRange(targetIp, sourceIp)) { + return true; + } + } + return false; + } + + public List getSourceIps() { + return sourceIps; + } + + public void setSourceIps(List sourceIps) { + this.sourceIps = sourceIps; + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/model/Policy.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/model/Policy.java new file mode 100644 index 0000000..fb4979d --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/model/Policy.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.auth.authorization.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import org.apache.commons.collections.CollectionUtils; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.auth.authorization.enums.Decision; +import org.apache.rocketmq.auth.authorization.enums.PolicyType; + +public class Policy { + + private PolicyType policyType; + + private List entries; + + public static Policy of(List resources, List actions, Environment environment, + Decision decision) { + return of(PolicyType.CUSTOM, resources, actions, environment, decision); + } + + public static Policy of(PolicyType policyType, List resources, List actions, + Environment environment, + Decision decision) { + Policy policy = new Policy(); + policy.setPolicyType(policyType); + List entries = resources.stream() + .map(resource -> PolicyEntry.of(resource, actions, environment, decision)) + .collect(Collectors.toList()); + policy.setEntries(entries); + return policy; + } + + public static Policy of(PolicyType type, List entries) { + Policy policy = new Policy(); + policy.setPolicyType(type); + policy.setEntries(entries); + return policy; + } + + public void updateEntry(List newEntries) { + if (this.entries == null) { + this.entries = new ArrayList<>(); + } + newEntries.forEach(newEntry -> { + PolicyEntry entry = getEntry(newEntry.getResource()); + if (entry == null) { + this.entries.add(newEntry); + } else { + entry.updateEntry(newEntry.getActions(), newEntry.getEnvironment(), newEntry.getDecision()); + } + }); + } + + public void deleteEntry(Resource resources) { + PolicyEntry entry = getEntry(resources); + if (entry != null) { + this.entries.remove(entry); + } + } + + private PolicyEntry getEntry(Resource resource) { + if (CollectionUtils.isEmpty(this.entries)) { + return null; + } + for (PolicyEntry entry : this.entries) { + if (Objects.equals(entry.getResource(), resource)) { + return entry; + } + } + return null; + } + + public PolicyType getPolicyType() { + return policyType; + } + + public void setPolicyType(PolicyType policyType) { + this.policyType = policyType; + } + + public List getEntries() { + return entries; + } + + public void setEntries(List entries) { + this.entries = entries; + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/model/PolicyEntry.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/model/PolicyEntry.java new file mode 100644 index 0000000..f119984 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/model/PolicyEntry.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.auth.authorization.model; + +import java.util.List; +import java.util.stream.Collectors; +import org.apache.commons.collections.CollectionUtils; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.auth.authorization.enums.Decision; + +public class PolicyEntry { + + private Resource resource; + + private List actions; + + private Environment environment; + + private Decision decision; + + public static PolicyEntry of(Resource resource, List actions, Environment environment, Decision decision) { + PolicyEntry policyEntry = new PolicyEntry(); + policyEntry.setResource(resource); + policyEntry.setActions(actions); + policyEntry.setEnvironment(environment); + policyEntry.setDecision(decision); + return policyEntry; + } + + public void updateEntry(List actions, Environment environment, + Decision decision) { + this.setActions(actions); + this.setEnvironment(environment); + this.setDecision(decision); + } + + public boolean isMatchResource(Resource resource) { + return this.resource.isMatch(resource); + } + + public boolean isMatchAction(List actions) { + if (CollectionUtils.isEmpty(this.actions)) { + return false; + } + if (actions.contains(Action.ANY)) { + return true; + } + return actions.stream() + .anyMatch(action -> this.actions.contains(action) + || this.actions.contains(Action.ALL)); + } + + public boolean isMatchEnvironment(Environment environment) { + if (this.environment == null) { + return true; + } + return this.environment.isMatch(environment); + } + + public String toResourceStr() { + if (resource == null) { + return null; + } + return resource.getResourceKey(); + } + + public List toActionsStr() { + if (CollectionUtils.isEmpty(actions)) { + return null; + } + return actions.stream().map(Action::getName) + .collect(Collectors.toList()); + } + + public Resource getResource() { + return resource; + } + + public void setResource(Resource resource) { + this.resource = resource; + } + + public List getActions() { + return actions; + } + + public void setActions(List actions) { + this.actions = actions; + } + + public Environment getEnvironment() { + return environment; + } + + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + public Decision getDecision() { + return decision; + } + + public void setDecision(Decision decision) { + this.decision = decision; + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/model/RequestContext.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/model/RequestContext.java new file mode 100644 index 0000000..88b814d --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/model/RequestContext.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.auth.authorization.model; + +import org.apache.rocketmq.auth.authentication.model.Subject; +import org.apache.rocketmq.common.action.Action; + +public class RequestContext { + + private Subject subject; + + private Resource resource; + + private Action action; + + private String sourceIp; + + public Subject getSubject() { + return subject; + } + + public void setSubject(Subject subject) { + this.subject = subject; + } + + public Resource getResource() { + return resource; + } + + public void setResource(Resource resource) { + this.resource = resource; + } + + public Action getAction() { + return action; + } + + public void setAction(Action action) { + this.action = action; + } + + public String getSourceIp() { + return sourceIp; + } + + public void setSourceIp(String sourceIp) { + this.sourceIp = sourceIp; + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/model/Resource.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/model/Resource.java new file mode 100644 index 0000000..67a3757 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/model/Resource.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.auth.authorization.model; + +import com.alibaba.fastjson2.annotation.JSONField; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.ResourcePattern; +import org.apache.rocketmq.common.constant.CommonConstants; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; + +public class Resource { + + private ResourceType resourceType; + + private String resourceName; + + private ResourcePattern resourcePattern; + + public static Resource ofCluster(String clusterName) { + return of(ResourceType.CLUSTER, clusterName, ResourcePattern.LITERAL); + } + + public static Resource ofTopic(String topicName) { + return of(ResourceType.TOPIC, topicName, ResourcePattern.LITERAL); + } + + public static Resource ofGroup(String groupName) { + if (NamespaceUtil.isRetryTopic(groupName)) { + groupName = NamespaceUtil.withOutRetryAndDLQ(groupName); + } + return of(ResourceType.GROUP, groupName, ResourcePattern.LITERAL); + } + + public static Resource of(ResourceType resourceType, String resourceName, ResourcePattern resourcePattern) { + Resource resource = new Resource(); + resource.resourceType = resourceType; + resource.resourceName = resourceName; + resource.resourcePattern = resourcePattern; + return resource; + } + + public static List of(List resourceKeys) { + if (CollectionUtils.isEmpty(resourceKeys)) { + return null; + } + return resourceKeys.stream().map(Resource::of).collect(Collectors.toList()); + } + + public static Resource of(String resourceKey) { + if (StringUtils.isBlank(resourceKey)) { + return null; + } + if (StringUtils.equals(resourceKey, CommonConstants.ASTERISK)) { + return of(ResourceType.ANY, null, ResourcePattern.ANY); + } + String type = StringUtils.substringBefore(resourceKey, CommonConstants.COLON); + ResourceType resourceType = ResourceType.getByName(type); + if (resourceType == null) { + return null; + } + String resourceName = StringUtils.substringAfter(resourceKey, CommonConstants.COLON); + ResourcePattern resourcePattern = ResourcePattern.LITERAL; + if (StringUtils.equals(resourceName, CommonConstants.ASTERISK)) { + resourceName = null; + resourcePattern = ResourcePattern.ANY; + } else if (StringUtils.endsWith(resourceName, CommonConstants.ASTERISK)) { + resourceName = StringUtils.substringBefore(resourceName, CommonConstants.ASTERISK); + resourcePattern = ResourcePattern.PREFIXED; + } + return of(resourceType, resourceName, resourcePattern); + } + + @JSONField(serialize = false) + public String getResourceKey() { + if (resourceType == ResourceType.ANY) { + return CommonConstants.ASTERISK; + } + switch (resourcePattern) { + case ANY: + return resourceType.getName() + CommonConstants.COLON + CommonConstants.ASTERISK; + case LITERAL: + return resourceType.getName() + CommonConstants.COLON + resourceName; + case PREFIXED: + return resourceType.getName() + CommonConstants.COLON + resourceName + CommonConstants.ASTERISK; + default: + return null; + } + } + + public boolean isMatch(Resource resource) { + if (this.resourceType == ResourceType.ANY) { + return true; + } + if (this.resourceType != resource.resourceType) { + return false; + } + switch (resourcePattern) { + case ANY: + return true; + case LITERAL: + return StringUtils.equals(resource.resourceName, this.resourceName); + case PREFIXED: + return StringUtils.startsWith(resource.resourceName, this.resourceName); + default: + return false; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Resource resource = (Resource) o; + return resourceType == resource.resourceType + && Objects.equals(resourceName, resource.resourceName) + && resourcePattern == resource.resourcePattern; + } + + @Override + public int hashCode() { + return Objects.hash(resourceType, resourceName, resourcePattern); + } + + public ResourceType getResourceType() { + return resourceType; + } + + public void setResourceType(ResourceType resourceType) { + this.resourceType = resourceType; + } + + public String getResourceName() { + return resourceName; + } + + public void setResourceName(String resourceName) { + this.resourceName = resourceName; + } + + public ResourcePattern getResourcePattern() { + return resourcePattern; + } + + public void setResourcePattern(ResourcePattern resourcePattern) { + this.resourcePattern = resourcePattern; + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/provider/AuthorizationMetadataProvider.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/provider/AuthorizationMetadataProvider.java new file mode 100644 index 0000000..02bae74 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/provider/AuthorizationMetadataProvider.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.auth.authorization.provider; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import org.apache.rocketmq.auth.authentication.model.Subject; +import org.apache.rocketmq.auth.authorization.model.Acl; +import org.apache.rocketmq.auth.config.AuthConfig; + +public interface AuthorizationMetadataProvider { + + void initialize(AuthConfig authConfig, Supplier metadataService); + + void shutdown(); + + CompletableFuture createAcl(Acl acl); + + CompletableFuture deleteAcl(Subject subject); + + CompletableFuture updateAcl(Acl acl); + + CompletableFuture getAcl(Subject subject); + + CompletableFuture> listAcl(String subjectFilter, String resourceFilter); +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/provider/AuthorizationProvider.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/provider/AuthorizationProvider.java new file mode 100644 index 0000000..98bd39c --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/provider/AuthorizationProvider.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.auth.authorization.provider; + +import com.google.protobuf.GeneratedMessageV3; +import io.grpc.Metadata; +import io.netty.channel.ChannelHandlerContext; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public interface AuthorizationProvider { + + void initialize(AuthConfig config); + + void initialize(AuthConfig config, Supplier metadataService); + + CompletableFuture authorize(AuthorizationContext context); + + List newContexts(Metadata metadata, GeneratedMessageV3 message); + + List newContexts(ChannelHandlerContext context, RemotingCommand command); +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/provider/DefaultAuthorizationProvider.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/provider/DefaultAuthorizationProvider.java new file mode 100644 index 0000000..7511103 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/provider/DefaultAuthorizationProvider.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.auth.authorization.provider; + +import com.google.protobuf.GeneratedMessageV3; +import io.grpc.Metadata; +import io.netty.channel.ChannelHandlerContext; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.apache.rocketmq.auth.authorization.builder.AuthorizationContextBuilder; +import org.apache.rocketmq.auth.authorization.builder.DefaultAuthorizationContextBuilder; +import org.apache.rocketmq.auth.authorization.chain.AclAuthorizationHandler; +import org.apache.rocketmq.auth.authorization.chain.UserAuthorizationHandler; +import org.apache.rocketmq.auth.authorization.context.DefaultAuthorizationContext; +import org.apache.rocketmq.auth.authorization.enums.Decision; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.chain.HandlerChain; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DefaultAuthorizationProvider implements AuthorizationProvider { + + protected final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_AUTH_AUDIT_LOGGER_NAME); + protected AuthConfig authConfig; + protected Supplier metadataService; + protected AuthorizationContextBuilder authorizationContextBuilder; + + @Override + public void initialize(AuthConfig config) { + this.initialize(config, null); + } + + @Override + public void initialize(AuthConfig config, Supplier metadataService) { + this.authConfig = config; + this.metadataService = metadataService; + this.authorizationContextBuilder = new DefaultAuthorizationContextBuilder(config); + } + + @Override + public CompletableFuture authorize(DefaultAuthorizationContext context) { + return this.newHandlerChain().handle(context) + .whenComplete((nil, ex) -> doAuditLog(context, ex)); + } + + @Override + public List newContexts(Metadata metadata, GeneratedMessageV3 message) { + return this.authorizationContextBuilder.build(metadata, message); + } + + @Override + public List newContexts(ChannelHandlerContext context, RemotingCommand command) { + return this.authorizationContextBuilder.build(context, command); + } + + protected HandlerChain> newHandlerChain() { + return HandlerChain.>create() + .addNext(new UserAuthorizationHandler(authConfig, metadataService)) + .addNext(new AclAuthorizationHandler(authConfig, metadataService)); + } + + protected void doAuditLog(DefaultAuthorizationContext context, Throwable ex) { + if (context.getSubject() == null) { + return; + } + Decision decision = Decision.ALLOW; + if (ex != null) { + decision = Decision.DENY; + } + String subject = context.getSubject().getSubjectKey(); + String actions = context.getActions().stream().map(Action::getName) + .collect(Collectors.joining(",")); + String sourceIp = context.getSourceIp(); + String resource = context.getResource().getResourceKey(); + String request = context.getRpcCode(); + String format = "[AUTHORIZATION] Subject = {} is {} Action = {} from sourceIp = {} on resource = {} for request = {}."; + if (decision == Decision.ALLOW) { + log.debug(format, subject, decision.getName(), actions, sourceIp, resource, request); + } else { + log.info(format, subject, decision.getName(), actions, sourceIp, resource, request); + } + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/provider/LocalAuthorizationMetadataProvider.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/provider/LocalAuthorizationMetadataProvider.java new file mode 100644 index 0000000..bc63178 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/provider/LocalAuthorizationMetadataProvider.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.auth.authorization.provider; + +import com.alibaba.fastjson2.JSON; +import com.github.benmanes.caffeine.cache.CacheLoader; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.auth.authentication.model.Subject; +import org.apache.rocketmq.auth.authorization.exception.AuthorizationException; +import org.apache.rocketmq.auth.authorization.model.Acl; +import org.apache.rocketmq.auth.authorization.model.Policy; +import org.apache.rocketmq.auth.authorization.model.PolicyEntry; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.common.config.ConfigRocksDBStorage; +import org.apache.rocketmq.common.thread.ThreadPoolMonitor; +import org.rocksdb.RocksIterator; + +public class LocalAuthorizationMetadataProvider implements AuthorizationMetadataProvider { + + private ConfigRocksDBStorage storage; + + private LoadingCache aclCache; + + @Override + public void initialize(AuthConfig authConfig, Supplier metadataService) { + this.storage = new ConfigRocksDBStorage(authConfig.getAuthConfigPath() + File.separator + "acls"); + if (!this.storage.start()) { + throw new RuntimeException("Failed to load rocksdb for auth_acl, please check whether it is occupied."); + } + ThreadPoolExecutor cacheRefreshExecutor = ThreadPoolMonitor.createAndMonitor( + 1, + 1, + 1000 * 60, + TimeUnit.MILLISECONDS, + "AclCacheRefresh", + 100000 + ); + + this.aclCache = Caffeine.newBuilder() + .maximumSize(authConfig.getAclCacheMaxNum()) + .expireAfterAccess(authConfig.getAclCacheExpiredSecond(), TimeUnit.SECONDS) + .refreshAfterWrite(authConfig.getAclCacheRefreshSecond(), TimeUnit.SECONDS) + .executor(cacheRefreshExecutor) + .build(new AclCacheLoader(this.storage)); + } + + @Override + public CompletableFuture createAcl(Acl acl) { + try { + Subject subject = acl.getSubject(); + byte[] keyBytes = subject.getSubjectKey().getBytes(StandardCharsets.UTF_8); + byte[] valueBytes = JSON.toJSONBytes(acl); + this.storage.put(keyBytes, keyBytes.length, valueBytes); + this.storage.flushWAL(); + this.aclCache.invalidate(subject.getSubjectKey()); + } catch (Exception e) { + throw new AuthorizationException("create Acl to RocksDB failed.", e); + } + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture deleteAcl(Subject subject) { + try { + byte[] keyBytes = subject.getSubjectKey().getBytes(StandardCharsets.UTF_8); + this.storage.delete(keyBytes); + this.storage.flushWAL(); + this.aclCache.invalidate(subject.getSubjectKey()); + } catch (Exception e) { + throw new AuthorizationException("delete Acl from RocksDB failed.", e); + } + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture updateAcl(Acl acl) { + try { + Subject subject = acl.getSubject(); + byte[] keyBytes = subject.getSubjectKey().getBytes(StandardCharsets.UTF_8); + byte[] valueBytes = JSON.toJSONBytes(acl); + this.storage.put(keyBytes, keyBytes.length, valueBytes); + this.storage.flushWAL(); + this.aclCache.invalidate(subject.getSubjectKey()); + } catch (Exception e) { + throw new AuthorizationException("update Acl to RocksDB failed.", e); + } + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture getAcl(Subject subject) { + Acl acl = aclCache.get(subject.getSubjectKey()); + if (acl == AclCacheLoader.EMPTY_ACL) { + return CompletableFuture.completedFuture(null); + } + return CompletableFuture.completedFuture(acl); + } + + @Override + public CompletableFuture> listAcl(String subjectFilter, String resourceFilter) { + List result = new ArrayList<>(); + try (RocksIterator iterator = this.storage.iterator()) { + iterator.seekToFirst(); + while (iterator.isValid()) { + String subjectKey = new String(iterator.key(), StandardCharsets.UTF_8); + if (StringUtils.isNotBlank(subjectFilter) && !subjectKey.contains(subjectFilter)) { + iterator.next(); + continue; + } + Subject subject = Subject.of(subjectKey); + Acl acl = JSON.parseObject(new String(iterator.value(), StandardCharsets.UTF_8), Acl.class); + List policies = acl.getPolicies(); + if (!CollectionUtils.isNotEmpty(policies)) { + iterator.next(); + continue; + } + Iterator policyIterator = policies.iterator(); + while (policyIterator.hasNext()) { + Policy policy = policyIterator.next(); + List entries = policy.getEntries(); + if (CollectionUtils.isEmpty(entries)) { + continue; + } + if (StringUtils.isNotBlank(resourceFilter) && !subjectKey.contains(resourceFilter)) { + entries.removeIf(entry -> !entry.toResourceStr().contains(resourceFilter)); + } + if (CollectionUtils.isEmpty(entries)) { + policyIterator.remove(); + } + } + if (CollectionUtils.isNotEmpty(policies)) { + result.add(Acl.of(subject, policies)); + } + iterator.next(); + } + } + return CompletableFuture.completedFuture(result); + } + + @Override + public void shutdown() { + if (this.storage != null) { + this.storage.shutdown(); + } + } + + private static class AclCacheLoader implements CacheLoader { + private final ConfigRocksDBStorage storage; + public static final Acl EMPTY_ACL = new Acl(); + + public AclCacheLoader(ConfigRocksDBStorage storage) { + this.storage = storage; + } + + @Override + public Acl load(String subjectKey) { + try { + byte[] keyBytes = subjectKey.getBytes(StandardCharsets.UTF_8); + Subject subject = Subject.of(subjectKey); + + byte[] valueBytes = this.storage.get(keyBytes); + if (ArrayUtils.isEmpty(valueBytes)) { + return EMPTY_ACL; + } + Acl acl = JSON.parseObject(valueBytes, Acl.class); + return Acl.of(subject, acl.getPolicies()); + } catch (Exception e) { + throw new AuthorizationException("get Acl from RocksDB failed.", e); + } + } + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/strategy/AbstractAuthorizationStrategy.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/strategy/AbstractAuthorizationStrategy.java new file mode 100644 index 0000000..849c308 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/strategy/AbstractAuthorizationStrategy.java @@ -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. + */ +package org.apache.rocketmq.auth.authorization.strategy; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.auth.authorization.context.AuthorizationContext; +import org.apache.rocketmq.auth.authorization.exception.AuthorizationException; +import org.apache.rocketmq.auth.authorization.factory.AuthorizationFactory; +import org.apache.rocketmq.auth.authorization.provider.AuthorizationProvider; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.common.utils.ExceptionUtils; + +public abstract class AbstractAuthorizationStrategy implements AuthorizationStrategy { + + protected final AuthConfig authConfig; + protected final List authorizationWhitelist = new ArrayList<>(); + protected final AuthorizationProvider authorizationProvider; + + public AbstractAuthorizationStrategy(AuthConfig authConfig, Supplier metadataService) { + this.authConfig = authConfig; + this.authorizationProvider = AuthorizationFactory.getProvider(authConfig); + if (this.authorizationProvider != null) { + this.authorizationProvider.initialize(authConfig, metadataService); + } + if (StringUtils.isNotBlank(authConfig.getAuthorizationWhitelist())) { + String[] whitelist = StringUtils.split(authConfig.getAuthorizationWhitelist(), ","); + for (String rpcCode : whitelist) { + this.authorizationWhitelist.add(StringUtils.trim(rpcCode)); + } + } + } + + public void doEvaluate(AuthorizationContext context) { + if (context == null) { + return; + } + if (!this.authConfig.isAuthorizationEnabled()) { + return; + } + if (this.authorizationProvider == null) { + return; + } + if (this.authorizationWhitelist.contains(context.getRpcCode())) { + return; + } + try { + this.authorizationProvider.authorize(context).join(); + } catch (AuthorizationException ex) { + throw ex; + } catch (Throwable ex) { + Throwable exception = ExceptionUtils.getRealException(ex); + if (exception instanceof AuthorizationException) { + throw (AuthorizationException) exception; + } + throw new AuthorizationException("Authorization failed. Please verify your access rights and try again.", exception); + } + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/strategy/AuthorizationStrategy.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/strategy/AuthorizationStrategy.java new file mode 100644 index 0000000..d65ca82 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/strategy/AuthorizationStrategy.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.auth.authorization.strategy; + +import org.apache.rocketmq.auth.authorization.context.AuthorizationContext; + +public interface AuthorizationStrategy { + + void evaluate(AuthorizationContext context); +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/strategy/StatefulAuthorizationStrategy.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/strategy/StatefulAuthorizationStrategy.java new file mode 100644 index 0000000..d5b252e --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/strategy/StatefulAuthorizationStrategy.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.auth.authorization.strategy; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.auth.authorization.context.AuthorizationContext; +import org.apache.rocketmq.auth.authorization.context.DefaultAuthorizationContext; +import org.apache.rocketmq.auth.authorization.exception.AuthorizationException; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.constant.CommonConstants; + +public class StatefulAuthorizationStrategy extends AbstractAuthorizationStrategy { + + protected Cache> authCache; + + public StatefulAuthorizationStrategy(AuthConfig authConfig, Supplier metadataService) { + super(authConfig, metadataService); + this.authCache = Caffeine.newBuilder() + .expireAfterWrite(authConfig.getStatefulAuthorizationCacheExpiredSecond(), TimeUnit.SECONDS) + .maximumSize(authConfig.getStatefulAuthorizationCacheMaxNum()) + .build(); + } + + @Override + public void evaluate(AuthorizationContext context) { + if (StringUtils.isBlank(context.getChannelId())) { + this.doEvaluate(context); + return; + } + Pair result = this.authCache.get(buildKey(context), key -> { + try { + this.doEvaluate(context); + return Pair.of(true, null); + } catch (AuthorizationException ex) { + return Pair.of(false, ex); + } + }); + if (result != null && result.getObject1() == Boolean.FALSE) { + throw result.getObject2(); + } + } + + private String buildKey(AuthorizationContext context) { + if (context instanceof DefaultAuthorizationContext) { + DefaultAuthorizationContext ctx = (DefaultAuthorizationContext) context; + return ctx.getChannelId() + + (ctx.getSubject() != null ? CommonConstants.POUND + ctx.getSubjectKey() : "") + + CommonConstants.POUND + ctx.getResourceKey() + + CommonConstants.POUND + StringUtils.join(ctx.getActions(), CommonConstants.COMMA) + + CommonConstants.POUND + ctx.getSourceIp(); + } + throw new AuthorizationException("The request of {} is not support.", context.getClass().getSimpleName()); + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/strategy/StatelessAuthorizationStrategy.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/strategy/StatelessAuthorizationStrategy.java new file mode 100644 index 0000000..e5d5e53 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/strategy/StatelessAuthorizationStrategy.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.auth.authorization.strategy; + +import java.util.function.Supplier; +import org.apache.rocketmq.auth.authorization.context.AuthorizationContext; +import org.apache.rocketmq.auth.config.AuthConfig; + +public class StatelessAuthorizationStrategy extends AbstractAuthorizationStrategy { + + public StatelessAuthorizationStrategy(AuthConfig authConfig, Supplier metadataService) { + super(authConfig, metadataService); + } + + @Override + public void evaluate(AuthorizationContext context) { + super.doEvaluate(context); + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/config/AuthConfig.java b/auth/src/main/java/org/apache/rocketmq/auth/config/AuthConfig.java new file mode 100644 index 0000000..ed294c8 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/config/AuthConfig.java @@ -0,0 +1,289 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.auth.config; + +public class AuthConfig implements Cloneable { + + private String configName; + + private String clusterName; + + private String authConfigPath; + + private boolean authenticationEnabled = false; + + private String authenticationProvider; + + private String authenticationMetadataProvider; + + private String authenticationStrategy; + + private String authenticationWhitelist; + + private String initAuthenticationUser; + + private String innerClientAuthenticationCredentials; + + private boolean authorizationEnabled = false; + + private String authorizationProvider; + + private String authorizationMetadataProvider; + + private String authorizationStrategy; + + private String authorizationWhitelist; + + private boolean migrateAuthFromV1Enabled = false; + + private int userCacheMaxNum = 1000; + + private int userCacheExpiredSecond = 600; + + private int userCacheRefreshSecond = 60; + + private int aclCacheMaxNum = 1000; + + private int aclCacheExpiredSecond = 600; + + private int aclCacheRefreshSecond = 60; + + private int statefulAuthenticationCacheMaxNum = 10000; + + private int statefulAuthenticationCacheExpiredSecond = 60; + + private int statefulAuthorizationCacheMaxNum = 10000; + + private int statefulAuthorizationCacheExpiredSecond = 60; + + @Override + public AuthConfig clone() { + try { + return (AuthConfig) super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError(); + } + } + + public String getConfigName() { + return configName; + } + + public void setConfigName(String configName) { + this.configName = configName; + } + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public String getAuthConfigPath() { + return authConfigPath; + } + + public void setAuthConfigPath(String authConfigPath) { + this.authConfigPath = authConfigPath; + } + + public boolean isAuthenticationEnabled() { + return authenticationEnabled; + } + + public void setAuthenticationEnabled(boolean authenticationEnabled) { + this.authenticationEnabled = authenticationEnabled; + } + + public String getAuthenticationProvider() { + return authenticationProvider; + } + + public void setAuthenticationProvider(String authenticationProvider) { + this.authenticationProvider = authenticationProvider; + } + + public String getAuthenticationMetadataProvider() { + return authenticationMetadataProvider; + } + + public void setAuthenticationMetadataProvider(String authenticationMetadataProvider) { + this.authenticationMetadataProvider = authenticationMetadataProvider; + } + + public String getAuthenticationStrategy() { + return authenticationStrategy; + } + + public void setAuthenticationStrategy(String authenticationStrategy) { + this.authenticationStrategy = authenticationStrategy; + } + + public String getAuthenticationWhitelist() { + return authenticationWhitelist; + } + + public void setAuthenticationWhitelist(String authenticationWhitelist) { + this.authenticationWhitelist = authenticationWhitelist; + } + + public String getInitAuthenticationUser() { + return initAuthenticationUser; + } + + public void setInitAuthenticationUser(String initAuthenticationUser) { + this.initAuthenticationUser = initAuthenticationUser; + } + + public String getInnerClientAuthenticationCredentials() { + return innerClientAuthenticationCredentials; + } + + public void setInnerClientAuthenticationCredentials(String innerClientAuthenticationCredentials) { + this.innerClientAuthenticationCredentials = innerClientAuthenticationCredentials; + } + + public boolean isAuthorizationEnabled() { + return authorizationEnabled; + } + + public void setAuthorizationEnabled(boolean authorizationEnabled) { + this.authorizationEnabled = authorizationEnabled; + } + + public String getAuthorizationProvider() { + return authorizationProvider; + } + + public void setAuthorizationProvider(String authorizationProvider) { + this.authorizationProvider = authorizationProvider; + } + + public String getAuthorizationMetadataProvider() { + return authorizationMetadataProvider; + } + + public void setAuthorizationMetadataProvider(String authorizationMetadataProvider) { + this.authorizationMetadataProvider = authorizationMetadataProvider; + } + + public String getAuthorizationStrategy() { + return authorizationStrategy; + } + + public void setAuthorizationStrategy(String authorizationStrategy) { + this.authorizationStrategy = authorizationStrategy; + } + + public String getAuthorizationWhitelist() { + return authorizationWhitelist; + } + + public void setAuthorizationWhitelist(String authorizationWhitelist) { + this.authorizationWhitelist = authorizationWhitelist; + } + + public boolean isMigrateAuthFromV1Enabled() { + return migrateAuthFromV1Enabled; + } + + public void setMigrateAuthFromV1Enabled(boolean migrateAuthFromV1Enabled) { + this.migrateAuthFromV1Enabled = migrateAuthFromV1Enabled; + } + + public int getUserCacheMaxNum() { + return userCacheMaxNum; + } + + public void setUserCacheMaxNum(int userCacheMaxNum) { + this.userCacheMaxNum = userCacheMaxNum; + } + + public int getUserCacheExpiredSecond() { + return userCacheExpiredSecond; + } + + public void setUserCacheExpiredSecond(int userCacheExpiredSecond) { + this.userCacheExpiredSecond = userCacheExpiredSecond; + } + + public int getUserCacheRefreshSecond() { + return userCacheRefreshSecond; + } + + public void setUserCacheRefreshSecond(int userCacheRefreshSecond) { + this.userCacheRefreshSecond = userCacheRefreshSecond; + } + + public int getAclCacheMaxNum() { + return aclCacheMaxNum; + } + + public void setAclCacheMaxNum(int aclCacheMaxNum) { + this.aclCacheMaxNum = aclCacheMaxNum; + } + + public int getAclCacheExpiredSecond() { + return aclCacheExpiredSecond; + } + + public void setAclCacheExpiredSecond(int aclCacheExpiredSecond) { + this.aclCacheExpiredSecond = aclCacheExpiredSecond; + } + + public int getAclCacheRefreshSecond() { + return aclCacheRefreshSecond; + } + + public void setAclCacheRefreshSecond(int aclCacheRefreshSecond) { + this.aclCacheRefreshSecond = aclCacheRefreshSecond; + } + + public int getStatefulAuthenticationCacheMaxNum() { + return statefulAuthenticationCacheMaxNum; + } + + public void setStatefulAuthenticationCacheMaxNum(int statefulAuthenticationCacheMaxNum) { + this.statefulAuthenticationCacheMaxNum = statefulAuthenticationCacheMaxNum; + } + + public int getStatefulAuthenticationCacheExpiredSecond() { + return statefulAuthenticationCacheExpiredSecond; + } + + public void setStatefulAuthenticationCacheExpiredSecond(int statefulAuthenticationCacheExpiredSecond) { + this.statefulAuthenticationCacheExpiredSecond = statefulAuthenticationCacheExpiredSecond; + } + + public int getStatefulAuthorizationCacheMaxNum() { + return statefulAuthorizationCacheMaxNum; + } + + public void setStatefulAuthorizationCacheMaxNum(int statefulAuthorizationCacheMaxNum) { + this.statefulAuthorizationCacheMaxNum = statefulAuthorizationCacheMaxNum; + } + + public int getStatefulAuthorizationCacheExpiredSecond() { + return statefulAuthorizationCacheExpiredSecond; + } + + public void setStatefulAuthorizationCacheExpiredSecond(int statefulAuthorizationCacheExpiredSecond) { + this.statefulAuthorizationCacheExpiredSecond = statefulAuthorizationCacheExpiredSecond; + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/migration/AuthMigrator.java b/auth/src/main/java/org/apache/rocketmq/auth/migration/AuthMigrator.java new file mode 100644 index 0000000..d2ab4dd --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/migration/AuthMigrator.java @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.auth.migration; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.acl.common.AclConstants; +import org.apache.rocketmq.auth.authentication.enums.UserType; +import org.apache.rocketmq.auth.authentication.factory.AuthenticationFactory; +import org.apache.rocketmq.auth.authentication.manager.AuthenticationMetadataManager; +import org.apache.rocketmq.auth.authentication.model.Subject; +import org.apache.rocketmq.auth.authentication.model.User; +import org.apache.rocketmq.auth.authorization.enums.Decision; +import org.apache.rocketmq.auth.authorization.enums.PolicyType; +import org.apache.rocketmq.auth.authorization.factory.AuthorizationFactory; +import org.apache.rocketmq.auth.authorization.manager.AuthorizationMetadataManager; +import org.apache.rocketmq.auth.authorization.model.Acl; +import org.apache.rocketmq.auth.authorization.model.Policy; +import org.apache.rocketmq.auth.authorization.model.PolicyEntry; +import org.apache.rocketmq.auth.authorization.model.Resource; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.auth.migration.v1.PlainPermissionManager; +import org.apache.rocketmq.auth.migration.v1.AclConfig; +import org.apache.rocketmq.auth.migration.v1.PlainAccessConfig; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.constant.CommonConstants; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.resource.ResourcePattern; +import org.apache.rocketmq.common.resource.ResourceType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +public class AuthMigrator { + + protected static final Logger LOG = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + + private final AuthConfig authConfig; + + private final PlainPermissionManager plainPermissionManager; + + private final AuthenticationMetadataManager authenticationMetadataManager; + + private final AuthorizationMetadataManager authorizationMetadataManager; + + public AuthMigrator(AuthConfig authConfig) { + this.authConfig = authConfig; + this.plainPermissionManager = new PlainPermissionManager(); + this.authenticationMetadataManager = AuthenticationFactory.getMetadataManager(authConfig); + this.authorizationMetadataManager = AuthorizationFactory.getMetadataManager(authConfig); + } + + public void migrate() { + if (!authConfig.isMigrateAuthFromV1Enabled()) { + return; + } + + AclConfig aclConfig = this.plainPermissionManager.getAllAclConfig(); + List accessConfigs = aclConfig.getPlainAccessConfigs(); + if (CollectionUtils.isEmpty(accessConfigs)) { + return; + } + + for (PlainAccessConfig accessConfig : accessConfigs) { + doMigrate(accessConfig); + } + } + + private void doMigrate(PlainAccessConfig accessConfig) { + this.isUserExisted(accessConfig.getAccessKey()).thenCompose(existed -> { + if (existed) { + return CompletableFuture.completedFuture(null); + } + return createUserAndAcl(accessConfig); + }).exceptionally(ex -> { + LOG.error("[ACL MIGRATE] An error occurred while migrating ACL configurations for AccessKey:{}.", accessConfig.getAccessKey(), ex); + return null; + }).join(); + } + + private CompletableFuture createUserAndAcl(PlainAccessConfig accessConfig) { + return createUser(accessConfig).thenCompose(nil -> createAcl(accessConfig)); + } + + private CompletableFuture createUser(PlainAccessConfig accessConfig) { + User user = new User(); + user.setUsername(accessConfig.getAccessKey()); + user.setPassword(accessConfig.getSecretKey()); + if (accessConfig.isAdmin()) { + user.setUserType(UserType.SUPER); + } else { + user.setUserType(UserType.NORMAL); + } + return this.authenticationMetadataManager.createUser(user); + } + + private CompletableFuture createAcl(PlainAccessConfig config) { + Subject subject = User.of(config.getAccessKey()); + List policies = new ArrayList<>(); + + Policy customPolicy = null; + if (CollectionUtils.isNotEmpty(config.getTopicPerms())) { + for (String topicPerm : config.getTopicPerms()) { + String[] temp = StringUtils.split(topicPerm, CommonConstants.EQUAL); + if (temp.length != 2) { + continue; + } + String topicName = StringUtils.trim(temp[0]); + String perm = StringUtils.trim(temp[1]); + Resource resource = Resource.ofTopic(topicName); + List actions = parseActions(perm); + Decision decision = parseDecision(perm); + PolicyEntry policyEntry = PolicyEntry.of(resource, actions, null, decision); + if (customPolicy == null) { + customPolicy = Policy.of(PolicyType.CUSTOM, new ArrayList<>()); + } + customPolicy.getEntries().add(policyEntry); + } + } + if (CollectionUtils.isNotEmpty(config.getGroupPerms())) { + for (String groupPerm : config.getGroupPerms()) { + String[] temp = StringUtils.split(groupPerm, CommonConstants.EQUAL); + if (temp.length != 2) { + continue; + } + String groupName = StringUtils.trim(temp[0]); + String perm = StringUtils.trim(temp[1]); + Resource resource = Resource.ofGroup(groupName); + List actions = parseActions(perm); + Decision decision = parseDecision(perm); + PolicyEntry policyEntry = PolicyEntry.of(resource, actions, null, decision); + if (customPolicy == null) { + customPolicy = Policy.of(PolicyType.CUSTOM, new ArrayList<>()); + } + customPolicy.getEntries().add(policyEntry); + } + } + if (customPolicy != null) { + policies.add(customPolicy); + } + + Policy defaultPolicy = null; + if (StringUtils.isNotBlank(config.getDefaultTopicPerm())) { + String topicPerm = StringUtils.trim(config.getDefaultTopicPerm()); + Resource resource = Resource.of(ResourceType.TOPIC, null, ResourcePattern.ANY); + List actions = parseActions(topicPerm); + Decision decision = parseDecision(topicPerm); + PolicyEntry policyEntry = PolicyEntry.of(resource, actions, null, decision); + defaultPolicy = Policy.of(PolicyType.DEFAULT, new ArrayList<>()); + defaultPolicy.getEntries().add(policyEntry); + } + if (StringUtils.isNotBlank(config.getDefaultGroupPerm())) { + String groupPerm = StringUtils.trim(config.getDefaultGroupPerm()); + Resource resource = Resource.of(ResourceType.GROUP, null, ResourcePattern.ANY); + List actions = parseActions(groupPerm); + Decision decision = parseDecision(groupPerm); + PolicyEntry policyEntry = PolicyEntry.of(resource, actions, null, decision); + if (defaultPolicy == null) { + defaultPolicy = Policy.of(PolicyType.DEFAULT, new ArrayList<>()); + } + defaultPolicy.getEntries().add(policyEntry); + } + if (defaultPolicy != null) { + policies.add(defaultPolicy); + } + + if (CollectionUtils.isEmpty(policies)) { + return CompletableFuture.completedFuture(null); + } + + Acl acl = Acl.of(subject, policies); + return this.authorizationMetadataManager.createAcl(acl); + } + + private Decision parseDecision(String str) { + if (StringUtils.isBlank(str)) { + return Decision.DENY; + } + return StringUtils.equals(str, AclConstants.DENY) ? Decision.DENY : Decision.ALLOW; + } + + private List parseActions(String str) { + List result = new ArrayList<>(); + if (StringUtils.isBlank(str)) { + result.add(Action.ALL); + } + switch (StringUtils.trim(str)) { + case AclConstants.PUB: + result.add(Action.PUB); + break; + case AclConstants.SUB: + result.add(Action.SUB); + break; + case AclConstants.PUB_SUB: + case AclConstants.SUB_PUB: + result.add(Action.PUB); + result.add(Action.SUB); + break; + case AclConstants.DENY: + result.add(Action.ALL); + break; + default: + result.add(Action.ALL); + break; + } + return result; + } + + private CompletableFuture isUserExisted(String username) { + return this.authenticationMetadataManager.getUser(username).thenApply(Objects::nonNull); + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/migration/v1/AccessResource.java b/auth/src/main/java/org/apache/rocketmq/auth/migration/v1/AccessResource.java new file mode 100644 index 0000000..0a706b7 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/migration/v1/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.auth.migration.v1; + +public interface AccessResource { +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/migration/v1/AclConfig.java b/auth/src/main/java/org/apache/rocketmq/auth/migration/v1/AclConfig.java new file mode 100644 index 0000000..dbea877 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/migration/v1/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.auth.migration.v1; + +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/auth/src/main/java/org/apache/rocketmq/auth/migration/v1/PlainAccessConfig.java b/auth/src/main/java/org/apache/rocketmq/auth/migration/v1/PlainAccessConfig.java new file mode 100644 index 0000000..f4368d6 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/migration/v1/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.auth.migration.v1; + +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/auth/src/main/java/org/apache/rocketmq/auth/migration/v1/PlainAccessData.java b/auth/src/main/java/org/apache/rocketmq/auth/migration/v1/PlainAccessData.java new file mode 100644 index 0000000..071a0ec --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/migration/v1/PlainAccessData.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.auth.migration.v1; + +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/auth/src/main/java/org/apache/rocketmq/auth/migration/v1/PlainAccessResource.java b/auth/src/main/java/org/apache/rocketmq/auth/migration/v1/PlainAccessResource.java new file mode 100644 index 0000000..edeb8e5 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/migration/v1/PlainAccessResource.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.auth.migration.v1; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.common.MixAll; + +import java.util.HashMap; +import java.util.Map; + +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 int requestCode; + + // The content to calculate the content + private byte[] content; + + private String signature; + + private String secretToken; + + private String recognition; + + public PlainAccessResource() { + } + + 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 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/auth/src/main/java/org/apache/rocketmq/auth/migration/v1/PlainPermissionManager.java b/auth/src/main/java/org/apache/rocketmq/auth/migration/v1/PlainPermissionManager.java new file mode 100644 index 0000000..7882859 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/migration/v1/PlainPermissionManager.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.auth.migration.v1; + +import org.apache.rocketmq.acl.common.AclException; +import org.apache.rocketmq.acl.common.AclUtils; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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.HashSet; +import java.util.List; +import java.util.Set; + +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 List fileList = new ArrayList<>(); + + + 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(); + } + + 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; files != null && 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; + } + + assureAclConfigFilesExist(); + + fileList = getAllAclFiles(defaultAclDir); + if (new File(defaultAclFile).exists() && !fileList.contains(defaultAclFile)) { + fileList.add(defaultAclFile); + } + } + + /** + * 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 AclConfig getAllAclConfig() { + AclConfig aclConfig = new AclConfig(); + List configs = new ArrayList<>(); + List whiteAddrs = new ArrayList<>(); + Set accessKeySets = new HashSet<>(); + + for (String path : fileList) { + 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 (PlainAccessConfig accessConfig : plainAccessConfigs) { + if (!accessKeySets.contains(accessConfig.getAccessKey())) { + accessKeySets.add(accessConfig.getAccessKey()); + PlainAccessConfig plainAccessConfig = new PlainAccessConfig(); + plainAccessConfig.setGroupPerms(accessConfig.getGroupPerms()); + plainAccessConfig.setDefaultTopicPerm(accessConfig.getDefaultTopicPerm()); + plainAccessConfig.setDefaultGroupPerm(accessConfig.getDefaultGroupPerm()); + plainAccessConfig.setAccessKey(accessConfig.getAccessKey()); + plainAccessConfig.setSecretKey(accessConfig.getSecretKey()); + plainAccessConfig.setAdmin(accessConfig.isAdmin()); + plainAccessConfig.setTopicPerms(accessConfig.getTopicPerms()); + plainAccessConfig.setWhiteRemoteAddress(accessConfig.getWhiteRemoteAddress()); + configs.add(plainAccessConfig); + } + } + } + } + aclConfig.setPlainAccessConfigs(configs); + aclConfig.setGlobalWhiteAddrs(whiteAddrs); + return aclConfig; + } +} diff --git a/auth/src/test/java/org/apache/rocketmq/auth/authentication/AuthenticationEvaluatorTest.java b/auth/src/test/java/org/apache/rocketmq/auth/authentication/AuthenticationEvaluatorTest.java new file mode 100644 index 0000000..dc20a0b --- /dev/null +++ b/auth/src/test/java/org/apache/rocketmq/auth/authentication/AuthenticationEvaluatorTest.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.auth.authentication; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import org.apache.commons.collections.CollectionUtils; +import org.apache.rocketmq.auth.authentication.context.DefaultAuthenticationContext; +import org.apache.rocketmq.auth.authentication.exception.AuthenticationException; +import org.apache.rocketmq.auth.authentication.factory.AuthenticationFactory; +import org.apache.rocketmq.auth.authentication.manager.AuthenticationMetadataManager; +import org.apache.rocketmq.auth.authentication.model.User; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.auth.helper.AuthTestHelper; +import org.apache.rocketmq.common.MixAll; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class AuthenticationEvaluatorTest { + + private AuthConfig authConfig; + private AuthenticationEvaluator evaluator; + private AuthenticationMetadataManager authenticationMetadataManager; + + @Before + public void setUp() throws Exception { + if (MixAll.isMac()) { + return; + } + this.authConfig = AuthTestHelper.createDefaultConfig(); + this.evaluator = new AuthenticationEvaluator(authConfig); + this.authenticationMetadataManager = AuthenticationFactory.getMetadataManager(authConfig); + this.clearAllUsers(); + } + + @After + public void tearDown() throws Exception { + if (MixAll.isMac()) { + return; + } + this.clearAllUsers(); + this.authenticationMetadataManager.shutdown(); + } + + @Test + public void evaluate1() { + if (MixAll.isMac()) { + return; + } + User user = User.of("test", "test"); + this.authenticationMetadataManager.createUser(user); + + DefaultAuthenticationContext context = new DefaultAuthenticationContext(); + context.setRpcCode("11"); + context.setUsername("test"); + context.setContent("test".getBytes(StandardCharsets.UTF_8)); + context.setSignature("DJRRXBXlCVuKh6ULoN87847QX+Y="); + this.evaluator.evaluate(context); + } + + @Test + public void evaluate2() { + if (MixAll.isMac()) { + return; + } + DefaultAuthenticationContext context = new DefaultAuthenticationContext(); + context.setRpcCode("11"); + context.setUsername("test"); + context.setContent("test".getBytes(StandardCharsets.UTF_8)); + context.setSignature("DJRRXBXlCVuKh6ULoN87847QX+Y="); + Assert.assertThrows(AuthenticationException.class, () -> this.evaluator.evaluate(context)); + } + + @Test + public void evaluate3() { + if (MixAll.isMac()) { + return; + } + User user = User.of("test", "test"); + this.authenticationMetadataManager.createUser(user); + + DefaultAuthenticationContext context = new DefaultAuthenticationContext(); + context.setRpcCode("11"); + context.setUsername("test"); + context.setContent("test".getBytes(StandardCharsets.UTF_8)); + context.setSignature("test"); + Assert.assertThrows(AuthenticationException.class, () -> this.evaluator.evaluate(context)); + } + + @Test + public void evaluate4() { + if (MixAll.isMac()) { + return; + } + this.authConfig.setAuthenticationWhitelist("11"); + this.evaluator = new AuthenticationEvaluator(authConfig); + + DefaultAuthenticationContext context = new DefaultAuthenticationContext(); + context.setRpcCode("11"); + context.setUsername("test"); + context.setContent("test".getBytes(StandardCharsets.UTF_8)); + context.setSignature("test"); + this.evaluator.evaluate(context); + } + + @Test + public void evaluate5() { + if (MixAll.isMac()) { + return; + } + this.authConfig.setAuthenticationEnabled(false); + this.evaluator = new AuthenticationEvaluator(authConfig); + + DefaultAuthenticationContext context = new DefaultAuthenticationContext(); + context.setRpcCode("11"); + context.setUsername("test"); + context.setContent("test".getBytes(StandardCharsets.UTF_8)); + context.setSignature("test"); + this.evaluator.evaluate(context); + } + + private void clearAllUsers() { + if (MixAll.isMac()) { + return; + } + List users = this.authenticationMetadataManager.listUser(null).join(); + if (CollectionUtils.isEmpty(users)) { + return; + } + users.forEach(user -> this.authenticationMetadataManager.deleteUser(user.getUsername()).join()); + } +} \ No newline at end of file diff --git a/auth/src/test/java/org/apache/rocketmq/auth/authentication/builder/DefaultAuthenticationContextBuilderTest.java b/auth/src/test/java/org/apache/rocketmq/auth/authentication/builder/DefaultAuthenticationContextBuilderTest.java new file mode 100644 index 0000000..e8e0144 --- /dev/null +++ b/auth/src/test/java/org/apache/rocketmq/auth/authentication/builder/DefaultAuthenticationContextBuilderTest.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.auth.authentication.builder; + +import apache.rocketmq.v2.Message; +import apache.rocketmq.v2.Resource; +import apache.rocketmq.v2.SendMessageRequest; +import com.google.protobuf.ByteString; +import io.grpc.Metadata; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelId; +import java.nio.charset.StandardCharsets; +import org.apache.rocketmq.auth.authentication.context.DefaultAuthenticationContext; +import org.apache.rocketmq.common.constant.GrpcConstants; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +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.Silent.class) +public class DefaultAuthenticationContextBuilderTest { + + private DefaultAuthenticationContextBuilder builder; + + @Mock + private ChannelHandlerContext channelHandlerContext; + + @Mock + private Channel channel; + + @Before + public void setUp() throws Exception { + builder = new DefaultAuthenticationContextBuilder(); + } + + @Test + public void build1() { + Resource topic = Resource.newBuilder().setName("topic-test").build(); + { + SendMessageRequest request = SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder().setTopic(topic) + .setBody(ByteString.copyFromUtf8("message-body")) + .build()) + .build(); + Metadata metadata = new Metadata(); + metadata.put(GrpcConstants.AUTHORIZATION, "MQv2-HMAC-SHA1 Credential=abc, SignedHeaders=x-mq-date-time, Signature=D18A9CBCDDBA9041D6693268FEF15A989E64430B"); + metadata.put(GrpcConstants.DATE_TIME, "20231227T194619Z"); + DefaultAuthenticationContext context = builder.build(metadata, request); + Assert.assertNotNull(context); + Assert.assertEquals("abc", context.getUsername()); + Assert.assertEquals("0YqcvN26kEHWaTJo/vFamJ5kQws=", context.getSignature()); + Assert.assertEquals("20231227T194619Z", new String(context.getContent(), StandardCharsets.UTF_8)); + } + } + + @Test + public void build2() { + when(channel.id()).thenReturn(mockChannelId("channel-id")); + when(channelHandlerContext.channel()).thenReturn(channel); + SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); + requestHeader.setTopic("topic-test"); + requestHeader.setQueueId(0); + requestHeader.setBornTimestamp(117036786441330L); + requestHeader.setBname("brokerName-1"); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, requestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "abc"); + request.addExtField("Signature", "ZG26exJ5u9q1fwZlO4DCmz2Rs88="); + request.makeCustomHeaderToNet(); + DefaultAuthenticationContext context = builder.build(channelHandlerContext, request); + Assert.assertNotNull(context); + Assert.assertEquals("abc", context.getUsername()); + Assert.assertEquals("ZG26exJ5u9q1fwZlO4DCmz2Rs88=", context.getSignature()); + Assert.assertEquals("abcbrokerName-11170367864413300topic-test", new String(context.getContent(), StandardCharsets.UTF_8)); + } + + private ChannelId mockChannelId(String channelId) { + return new ChannelId() { + @Override + public String asShortText() { + return channelId; + } + + @Override + public String asLongText() { + return channelId; + } + + @Override + public int compareTo(ChannelId o) { + return 0; + } + }; + } +} \ No newline at end of file diff --git a/auth/src/test/java/org/apache/rocketmq/auth/authentication/manager/AuthenticationMetadataManagerTest.java b/auth/src/test/java/org/apache/rocketmq/auth/authentication/manager/AuthenticationMetadataManagerTest.java new file mode 100644 index 0000000..844deb3 --- /dev/null +++ b/auth/src/test/java/org/apache/rocketmq/auth/authentication/manager/AuthenticationMetadataManagerTest.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.auth.authentication.manager; + +import java.util.List; +import org.apache.commons.collections.CollectionUtils; +import org.apache.rocketmq.auth.authentication.enums.UserType; +import org.apache.rocketmq.auth.authentication.exception.AuthenticationException; +import org.apache.rocketmq.auth.authentication.factory.AuthenticationFactory; +import org.apache.rocketmq.auth.authentication.model.User; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.auth.helper.AuthTestHelper; +import org.apache.rocketmq.common.MixAll; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class AuthenticationMetadataManagerTest { + + private AuthConfig authConfig; + private AuthenticationMetadataManager authenticationMetadataManager; + + @Before + public void setUp() throws Exception { + if (MixAll.isMac()) { + return; + } + this.authConfig = AuthTestHelper.createDefaultConfig(); + this.authenticationMetadataManager = AuthenticationFactory.getMetadataManager(this.authConfig); + this.clearAllUsers(); + } + + @After + public void tearDown() throws Exception { + if (MixAll.isMac()) { + return; + } + this.clearAllUsers(); + this.authenticationMetadataManager.shutdown(); + } + + @Test + public void createUser() { + if (MixAll.isMac()) { + return; + } + User user = User.of("test", "test"); + this.authenticationMetadataManager.createUser(user).join(); + user = this.authenticationMetadataManager.getUser("test").join(); + Assert.assertNotNull(user); + Assert.assertEquals(user.getUsername(), "test"); + Assert.assertEquals(user.getPassword(), "test"); + Assert.assertEquals(user.getUserType(), UserType.NORMAL); + + user = User.of("super", "super", UserType.SUPER); + this.authenticationMetadataManager.createUser(user).join(); + user = this.authenticationMetadataManager.getUser("super").join(); + Assert.assertNotNull(user); + Assert.assertEquals(user.getUsername(), "super"); + Assert.assertEquals(user.getPassword(), "super"); + Assert.assertEquals(user.getUserType(), UserType.SUPER); + + Assert.assertThrows(AuthenticationException.class, () -> { + try { + User user2 = User.of("test", "test"); + this.authenticationMetadataManager.createUser(user2).join(); + } catch (Exception e) { + AuthTestHelper.handleException(e); + } + }); + } + + @Test + public void updateUser() { + if (MixAll.isMac()) { + return; + } + User user = User.of("test", "test"); + this.authenticationMetadataManager.createUser(user).join(); + user = this.authenticationMetadataManager.getUser("test").join(); + Assert.assertNotNull(user); + Assert.assertEquals(user.getUsername(), "test"); + Assert.assertEquals(user.getPassword(), "test"); + Assert.assertEquals(user.getUserType(), UserType.NORMAL); + + user.setPassword("123"); + this.authenticationMetadataManager.updateUser(user).join(); + user = this.authenticationMetadataManager.getUser("test").join(); + Assert.assertNotNull(user); + Assert.assertEquals(user.getUsername(), "test"); + Assert.assertEquals(user.getPassword(), "123"); + Assert.assertEquals(user.getUserType(), UserType.NORMAL); + + user.setUserType(UserType.SUPER); + this.authenticationMetadataManager.updateUser(user).join(); + user = this.authenticationMetadataManager.getUser("test").join(); + Assert.assertNotNull(user); + Assert.assertEquals(user.getUsername(), "test"); + Assert.assertEquals(user.getPassword(), "123"); + Assert.assertEquals(user.getUserType(), UserType.SUPER); + + Assert.assertThrows(AuthenticationException.class, () -> { + try { + User user2 = User.of("no_user", "no_user"); + this.authenticationMetadataManager.updateUser(user2).join(); + } catch (Exception e) { + AuthTestHelper.handleException(e); + } + }); + } + + @Test + public void deleteUser() { + if (MixAll.isMac()) { + return; + } + User user = User.of("test", "test"); + this.authenticationMetadataManager.createUser(user).join(); + user = this.authenticationMetadataManager.getUser("test").join(); + Assert.assertNotNull(user); + this.authenticationMetadataManager.deleteUser("test").join(); + user = this.authenticationMetadataManager.getUser("test").join(); + Assert.assertNull(user); + + this.authenticationMetadataManager.deleteUser("no_user").join(); + } + + @Test + public void getUser() { + if (MixAll.isMac()) { + return; + } + User user = User.of("test", "test"); + this.authenticationMetadataManager.createUser(user).join(); + user = this.authenticationMetadataManager.getUser("test").join(); + Assert.assertNotNull(user); + Assert.assertEquals(user.getUsername(), "test"); + Assert.assertEquals(user.getPassword(), "test"); + Assert.assertEquals(user.getUserType(), UserType.NORMAL); + + user = this.authenticationMetadataManager.getUser("no_user").join(); + Assert.assertNull(user); + } + + @Test + public void listUser() { + if (MixAll.isMac()) { + return; + } + List users = this.authenticationMetadataManager.listUser(null).join(); + Assert.assertTrue(CollectionUtils.isEmpty(users)); + + User user = User.of("test-1", "test-1"); + this.authenticationMetadataManager.createUser(user).join(); + users = this.authenticationMetadataManager.listUser(null).join(); + Assert.assertEquals(users.size(), 1); + + user = User.of("test-2", "test-2"); + this.authenticationMetadataManager.createUser(user).join(); + users = this.authenticationMetadataManager.listUser("test").join(); + Assert.assertEquals(users.size(), 2); + } + + private void clearAllUsers() { + List users = this.authenticationMetadataManager.listUser(null).join(); + if (CollectionUtils.isEmpty(users)) { + return; + } + users.forEach(user -> this.authenticationMetadataManager.deleteUser(user.getUsername()).join()); + } +} \ No newline at end of file diff --git a/auth/src/test/java/org/apache/rocketmq/auth/authorization/AuthorizationEvaluatorTest.java b/auth/src/test/java/org/apache/rocketmq/auth/authorization/AuthorizationEvaluatorTest.java new file mode 100644 index 0000000..d8b839d --- /dev/null +++ b/auth/src/test/java/org/apache/rocketmq/auth/authorization/AuthorizationEvaluatorTest.java @@ -0,0 +1,363 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.auth.authorization; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.apache.commons.collections.CollectionUtils; +import org.apache.rocketmq.auth.authentication.factory.AuthenticationFactory; +import org.apache.rocketmq.auth.authentication.manager.AuthenticationMetadataManager; +import org.apache.rocketmq.auth.authentication.model.Subject; +import org.apache.rocketmq.auth.authentication.model.User; +import org.apache.rocketmq.auth.authorization.context.AuthorizationContext; +import org.apache.rocketmq.auth.authorization.context.DefaultAuthorizationContext; +import org.apache.rocketmq.auth.authorization.enums.Decision; +import org.apache.rocketmq.auth.authorization.enums.PolicyType; +import org.apache.rocketmq.auth.authorization.exception.AuthorizationException; +import org.apache.rocketmq.auth.authorization.factory.AuthorizationFactory; +import org.apache.rocketmq.auth.authorization.manager.AuthorizationMetadataManager; +import org.apache.rocketmq.auth.authorization.model.Acl; +import org.apache.rocketmq.auth.authorization.model.Resource; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.auth.helper.AuthTestHelper; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.action.Action; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class AuthorizationEvaluatorTest { + + private AuthConfig authConfig; + private AuthorizationEvaluator evaluator; + private AuthenticationMetadataManager authenticationMetadataManager; + private AuthorizationMetadataManager authorizationMetadataManager; + + @Before + public void setUp() throws Exception { + if (MixAll.isMac()) { + return; + } + this.authConfig = AuthTestHelper.createDefaultConfig(); + this.evaluator = new AuthorizationEvaluator(authConfig); + this.authenticationMetadataManager = AuthenticationFactory.getMetadataManager(authConfig); + this.authorizationMetadataManager = AuthorizationFactory.getMetadataManager(authConfig); + this.clearAllAcls(); + this.clearAllUsers(); + } + + @After + public void tearDown() throws Exception { + if (MixAll.isMac()) { + return; + } + this.clearAllAcls(); + this.clearAllUsers(); + this.authenticationMetadataManager.shutdown(); + } + + @Test + public void evaluate1() { + if (MixAll.isMac()) { + return; + } + User user = User.of("test", "test"); + this.authenticationMetadataManager.createUser(user).join(); + + Acl acl = AuthTestHelper.buildAcl("User:test", "Topic:test*", "Pub", "192.168.0.0/24", Decision.ALLOW); + this.authorizationMetadataManager.createAcl(acl).join(); + + Subject subject = Subject.of("User:test"); + Resource resource = Resource.ofTopic("test"); + Action action = Action.PUB; + String sourceIp = "192.168.0.1"; + DefaultAuthorizationContext context = DefaultAuthorizationContext.of(subject, resource, action, sourceIp); + context.setRpcCode("10"); + this.evaluator.evaluate(Collections.singletonList(context)); + + // acl sourceIp is null + acl = AuthTestHelper.buildAcl("User:test", "Topic:test*", "Pub", null, Decision.ALLOW); + this.authorizationMetadataManager.updateAcl(acl).join(); + + subject = Subject.of("User:test"); + resource = Resource.ofTopic("test"); + action = Action.PUB; + sourceIp = "192.168.0.1"; + context = DefaultAuthorizationContext.of(subject, resource, action, sourceIp); + context.setRpcCode("10"); + this.evaluator.evaluate(Collections.singletonList(context)); + } + + @Test + public void evaluate2() { + if (MixAll.isMac()) { + return; + } + User user = User.of("test", "test"); + this.authenticationMetadataManager.createUser(user).join(); + + Acl acl = AuthTestHelper.buildAcl("User:test", "Topic:test*,Group:test*", "Sub", "192.168.0.0/24", Decision.ALLOW); + this.authorizationMetadataManager.createAcl(acl).join(); + + List contexts = new ArrayList<>(); + + Subject subject = Subject.of("User:test"); + Resource resource = Resource.ofTopic("test"); + Action action = Action.SUB; + String sourceIp = "192.168.0.1"; + DefaultAuthorizationContext context1 = DefaultAuthorizationContext.of(subject, resource, action, sourceIp); + context1.setRpcCode("11"); + contexts.add(context1); + + subject = Subject.of("User:test"); + resource = Resource.ofGroup("test"); + action = Action.SUB; + sourceIp = "192.168.0.1"; + DefaultAuthorizationContext context2 = DefaultAuthorizationContext.of(subject, resource, action, sourceIp); + context2.setRpcCode("11"); + contexts.add(context2); + + this.evaluator.evaluate(contexts); + } + + @Test + public void evaluate4() { + if (MixAll.isMac()) { + return; + } + User user = User.of("test", "test"); + this.authenticationMetadataManager.createUser(user).join(); + + Acl acl = AuthTestHelper.buildAcl("User:test", "Topic:test*", "Pub", "192.168.0.0/24", Decision.ALLOW); + this.authorizationMetadataManager.createAcl(acl).join(); + + // user not exist + Assert.assertThrows(AuthorizationException.class, () -> { + Subject subject = Subject.of("User:abc"); + Resource resource = Resource.ofTopic("test"); + Action action = Action.PUB; + String sourceIp = "192.168.0.1"; + DefaultAuthorizationContext context = DefaultAuthorizationContext.of(subject, resource, action, sourceIp); + context.setRpcCode("10"); + this.evaluator.evaluate(Collections.singletonList(context)); + }); + + // resource not match + Assert.assertThrows(AuthorizationException.class, () -> { + Subject subject = Subject.of("User:test"); + Resource resource = Resource.ofTopic("abc"); + Action action = Action.PUB; + String sourceIp = "192.168.0.1"; + DefaultAuthorizationContext context = DefaultAuthorizationContext.of(subject, resource, action, sourceIp); + context.setRpcCode("10"); + this.evaluator.evaluate(Collections.singletonList(context)); + }); + + // action not match + Assert.assertThrows(AuthorizationException.class, () -> { + Subject subject = Subject.of("User:test"); + Resource resource = Resource.ofTopic("test"); + Action action = Action.SUB; + String sourceIp = "192.168.0.1"; + DefaultAuthorizationContext context = DefaultAuthorizationContext.of(subject, resource, action, sourceIp); + context.setRpcCode("10"); + this.evaluator.evaluate(Collections.singletonList(context)); + }); + + // sourceIp not match + Assert.assertThrows(AuthorizationException.class, () -> { + Subject subject = Subject.of("User:test"); + Resource resource = Resource.ofTopic("test"); + Action action = Action.PUB; + String sourceIp = "10.10.0.1"; + DefaultAuthorizationContext context = DefaultAuthorizationContext.of(subject, resource, action, sourceIp); + context.setRpcCode("10"); + this.evaluator.evaluate(Collections.singletonList(context)); + }); + + // decision is deny + acl = AuthTestHelper.buildAcl("User:test", "Topic:test*", "Pub", "192.168.0.0/24", Decision.DENY); + this.authorizationMetadataManager.updateAcl(acl).join(); + Assert.assertThrows(AuthorizationException.class, () -> { + Subject subject = Subject.of("User:test"); + Resource resource = Resource.ofTopic("test"); + Action action = Action.PUB; + String sourceIp = "192.168.0.1"; + DefaultAuthorizationContext context = DefaultAuthorizationContext.of(subject, resource, action, sourceIp); + context.setRpcCode("10"); + this.evaluator.evaluate(Collections.singletonList(context)); + }); + } + + @Test + public void evaluate5() { + if (MixAll.isMac()) { + return; + } + User user = User.of("test", "test"); + this.authenticationMetadataManager.createUser(user).join(); + + Acl acl = AuthTestHelper.buildAcl("User:test", "*", "Pub,Sub", "192.168.0.0/24", Decision.ALLOW); + this.authorizationMetadataManager.createAcl(acl).join(); + + acl = AuthTestHelper.buildAcl("User:test", "Topic:*", "Pub,Sub", "192.168.0.0/24", Decision.DENY); + this.authorizationMetadataManager.updateAcl(acl).join(); + + acl = AuthTestHelper.buildAcl("User:test", "Topic:test*", "Pub,Sub", "192.168.0.0/24", Decision.ALLOW); + this.authorizationMetadataManager.updateAcl(acl).join(); + + acl = AuthTestHelper.buildAcl("User:test", "Topic:test-1", "Pub,Sub", "192.168.0.0/24", Decision.DENY); + this.authorizationMetadataManager.updateAcl(acl).join(); + + Assert.assertThrows(AuthorizationException.class, () -> { + Subject subject = Subject.of("User:test"); + Resource resource = Resource.ofTopic("test-1"); + Action action = Action.PUB; + String sourceIp = "192.168.0.1"; + DefaultAuthorizationContext context = DefaultAuthorizationContext.of(subject, resource, action, sourceIp); + context.setRpcCode("10"); + this.evaluator.evaluate(Collections.singletonList(context)); + }); + + { + Subject subject = Subject.of("User:test"); + Resource resource = Resource.ofTopic("test-2"); + Action action = Action.PUB; + String sourceIp = "192.168.0.1"; + DefaultAuthorizationContext context = DefaultAuthorizationContext.of(subject, resource, action, sourceIp); + context.setRpcCode("10"); + this.evaluator.evaluate(Collections.singletonList(context)); + } + + Assert.assertThrows(AuthorizationException.class, () -> { + Subject subject = Subject.of("User:test"); + Resource resource = Resource.ofTopic("abc"); + Action action = Action.PUB; + String sourceIp = "192.168.0.1"; + DefaultAuthorizationContext context = DefaultAuthorizationContext.of(subject, resource, action, sourceIp); + context.setRpcCode("10"); + this.evaluator.evaluate(Collections.singletonList(context)); + }); + + { + Subject subject = Subject.of("User:test"); + Resource resource = Resource.ofGroup("test-2"); + Action action = Action.SUB; + String sourceIp = "192.168.0.1"; + DefaultAuthorizationContext context = DefaultAuthorizationContext.of(subject, resource, action, sourceIp); + context.setRpcCode("10"); + this.evaluator.evaluate(Collections.singletonList(context)); + } + } + + @Test + public void evaluate6() { + if (MixAll.isMac()) { + return; + } + this.authConfig.setAuthorizationWhitelist("10"); + this.evaluator = new AuthorizationEvaluator(this.authConfig); + + Subject subject = Subject.of("User:test"); + Resource resource = Resource.ofTopic("test"); + Action action = Action.PUB; + String sourceIp = "192.168.0.1"; + DefaultAuthorizationContext context = DefaultAuthorizationContext.of(subject, resource, action, sourceIp); + context.setRpcCode("10"); + this.evaluator.evaluate(Collections.singletonList(context)); + } + + @Test + public void evaluate7() { + if (MixAll.isMac()) { + return; + } + this.authConfig.setAuthorizationEnabled(false); + this.evaluator = new AuthorizationEvaluator(this.authConfig); + + Subject subject = Subject.of("User:test"); + Resource resource = Resource.ofTopic("test"); + Action action = Action.PUB; + String sourceIp = "192.168.0.1"; + DefaultAuthorizationContext context = DefaultAuthorizationContext.of(subject, resource, action, sourceIp); + context.setRpcCode("10"); + this.evaluator.evaluate(Collections.singletonList(context)); + } + + @Test + public void evaluate8() { + if (MixAll.isMac()) { + return; + } + User user = User.of("test", "test"); + this.authenticationMetadataManager.createUser(user).join(); + + Acl acl = AuthTestHelper.buildAcl("User:test", "Topic:test*", "Pub", "192.168.0.0/24", Decision.DENY); + this.authorizationMetadataManager.createAcl(acl).join(); + + + Assert.assertThrows(AuthorizationException.class, () -> { + Subject subject = Subject.of("User:test"); + Resource resource = Resource.ofTopic("test"); + Action action = Action.PUB; + String sourceIp = "192.168.0.1"; + DefaultAuthorizationContext context = DefaultAuthorizationContext.of(subject, resource, action, sourceIp); + context.setRpcCode("10"); + this.evaluator.evaluate(Collections.singletonList(context)); + }); + + Assert.assertThrows(AuthorizationException.class, () -> { + Subject subject = Subject.of("User:test"); + Resource resource = Resource.ofTopic("abc"); + Action action = Action.PUB; + String sourceIp = "192.168.0.1"; + DefaultAuthorizationContext context = DefaultAuthorizationContext.of(subject, resource, action, sourceIp); + context.setRpcCode("10"); + this.evaluator.evaluate(Collections.singletonList(context)); + }); + + acl = AuthTestHelper.buildAcl("User:test", PolicyType.DEFAULT, "Topic:*", "Pub", null, Decision.ALLOW); + this.authorizationMetadataManager.updateAcl(acl).join(); + { + Subject subject = Subject.of("User:test"); + Resource resource = Resource.ofTopic("abc"); + Action action = Action.PUB; + String sourceIp = "192.168.0.1"; + DefaultAuthorizationContext context = DefaultAuthorizationContext.of(subject, resource, action, sourceIp); + context.setRpcCode("10"); + this.evaluator.evaluate(Collections.singletonList(context)); + } + } + + private void clearAllUsers() { + List users = this.authenticationMetadataManager.listUser(null).join(); + if (CollectionUtils.isEmpty(users)) { + return; + } + users.forEach(user -> this.authenticationMetadataManager.deleteUser(user.getUsername()).join()); + } + + private void clearAllAcls() { + List acls = this.authorizationMetadataManager.listAcl(null, null).join(); + if (CollectionUtils.isEmpty(acls)) { + return; + } + acls.forEach(acl -> this.authorizationMetadataManager.deleteAcl(acl.getSubject(), null, null).join()); + } +} \ No newline at end of file diff --git a/auth/src/test/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilderTest.java b/auth/src/test/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilderTest.java new file mode 100644 index 0000000..c73e07d --- /dev/null +++ b/auth/src/test/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilderTest.java @@ -0,0 +1,580 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.auth.authorization.builder; + +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.MessageQueue; +import apache.rocketmq.v2.NotifyClientTerminationRequest; +import apache.rocketmq.v2.Publishing; +import apache.rocketmq.v2.QueryAssignmentRequest; +import apache.rocketmq.v2.QueryRouteRequest; +import apache.rocketmq.v2.RecallMessageRequest; +import apache.rocketmq.v2.ReceiveMessageRequest; +import apache.rocketmq.v2.Resource; +import apache.rocketmq.v2.SendMessageRequest; +import apache.rocketmq.v2.Settings; +import apache.rocketmq.v2.Subscription; +import apache.rocketmq.v2.SubscriptionEntry; +import apache.rocketmq.v2.TelemetryCommand; +import com.alibaba.fastjson2.JSON; +import com.google.common.collect.Sets; +import com.google.protobuf.GeneratedMessageV3; +import io.grpc.Metadata; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelId; +import io.netty.util.Attribute; +import io.netty.util.AttributeKey; +import java.util.Arrays; +import java.util.List; +import org.apache.rocketmq.auth.authorization.context.DefaultAuthorizationContext; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.common.TopicFilterType; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.constant.GrpcConstants; +import org.apache.rocketmq.common.resource.ResourceType; +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.RequestHeaderRegistry; +import org.apache.rocketmq.remoting.protocol.header.ConsumerSendMsgBackRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.CreateTopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.CreateUserRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.EndTransactionRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.HeartbeatRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.RecallMessageRequestHeader; +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.SubscriptionData; +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.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class DefaultAuthorizationContextBuilderTest { + + private AuthorizationContextBuilder builder; + + @Mock + private ChannelHandlerContext channelHandlerContext; + + @Mock + private Channel channel; + + @Before + public void setUp() throws Exception { + AuthConfig authConfig = new AuthConfig(); + authConfig.setClusterName("DefaultCluster"); + builder = new DefaultAuthorizationContextBuilder(authConfig); + RequestHeaderRegistry.getInstance().initialize(); + } + + @Test + public void buildGrpc() { + Metadata metadata = new Metadata(); + metadata.put(GrpcConstants.AUTHORIZATION_AK, "rocketmq"); + metadata.put(GrpcConstants.REMOTE_ADDRESS, "192.168.0.1"); + metadata.put(GrpcConstants.CHANNEL_ID, "channel-id"); + + GeneratedMessageV3 request = SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder().setName("topic").build()) + .build()) + .build(); + List result = builder.build(metadata, request); + Assert.assertEquals(1, result.size()); + Assert.assertEquals(result.get(0).getSubject().getSubjectKey(), "User:rocketmq"); + Assert.assertEquals(result.get(0).getResource().getResourceKey(), "Topic:topic"); + Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.PUB))); + Assert.assertEquals(result.get(0).getSourceIp(), "192.168.0.1"); + Assert.assertEquals(result.get(0).getChannelId(), "channel-id"); + Assert.assertEquals(result.get(0).getRpcCode(), SendMessageRequest.getDescriptor().getFullName()); + + request = RecallMessageRequest.newBuilder() + .setTopic(Resource.newBuilder().setName("topic").build()) + .setRecallHandle("handle") + .build(); + result = builder.build(metadata, request); + Assert.assertEquals(1, result.size()); + Assert.assertEquals(result.get(0).getSubject().getSubjectKey(), "User:rocketmq"); + Assert.assertEquals(result.get(0).getResource().getResourceKey(), "Topic:topic"); + Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.PUB))); + Assert.assertEquals(result.get(0).getSourceIp(), "192.168.0.1"); + Assert.assertEquals(result.get(0).getChannelId(), "channel-id"); + Assert.assertEquals(result.get(0).getRpcCode(), RecallMessageRequest.getDescriptor().getFullName()); + + request = EndTransactionRequest.newBuilder() + .setTopic(Resource.newBuilder().setName("topic").build()) + .build(); + result = builder.build(metadata, request); + Assert.assertEquals(1, result.size()); + Assert.assertEquals(result.get(0).getSubject().getSubjectKey(), "User:rocketmq"); + Assert.assertEquals(result.get(0).getResource().getResourceKey(), "Topic:topic"); + Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.PUB))); + + request = HeartbeatRequest.newBuilder() + .setClientType(ClientType.PUSH_CONSUMER) + .setGroup(Resource.newBuilder().setName("group").build()) + .build(); + result = builder.build(metadata, request); + Assert.assertEquals(1, result.size()); + Assert.assertEquals(result.get(0).getSubject().getSubjectKey(), "User:rocketmq"); + Assert.assertEquals(result.get(0).getResource().getResourceKey(), "Group:group"); + Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.SUB))); + + request = ReceiveMessageRequest.newBuilder() + .setMessageQueue(MessageQueue.newBuilder() + .setTopic(Resource.newBuilder().setName("topic").build()) + .build()) + .setGroup(Resource.newBuilder().setName("group").build()) + .build(); + result = builder.build(metadata, request); + Assert.assertEquals(2, result.size()); + Assert.assertEquals(getContext(result, ResourceType.GROUP).getSubject().getSubjectKey(), "User:rocketmq"); + Assert.assertEquals(getContext(result, ResourceType.GROUP).getResource().getResourceKey(), "Group:group"); + Assert.assertTrue(getContext(result, ResourceType.GROUP).getActions().containsAll(Arrays.asList(Action.SUB))); + Assert.assertEquals(getContext(result, ResourceType.TOPIC).getSubject().getSubjectKey(), "User:rocketmq"); + Assert.assertEquals(getContext(result, ResourceType.TOPIC).getResource().getResourceKey(), "Topic:topic"); + Assert.assertTrue(getContext(result, ResourceType.TOPIC).getActions().containsAll(Arrays.asList(Action.SUB))); + + request = AckMessageRequest.newBuilder() + .setTopic(Resource.newBuilder().setName("topic").build()) + .setGroup(Resource.newBuilder().setName("group").build()) + .build(); + result = builder.build(metadata, request); + Assert.assertEquals(2, result.size()); + Assert.assertEquals(getContext(result, ResourceType.GROUP).getSubject().getSubjectKey(), "User:rocketmq"); + Assert.assertEquals(getContext(result, ResourceType.GROUP).getResource().getResourceKey(), "Group:group"); + Assert.assertTrue(getContext(result, ResourceType.GROUP).getActions().containsAll(Arrays.asList(Action.SUB))); + Assert.assertEquals(getContext(result, ResourceType.TOPIC).getSubject().getSubjectKey(), "User:rocketmq"); + Assert.assertEquals(getContext(result, ResourceType.TOPIC).getResource().getResourceKey(), "Topic:topic"); + Assert.assertTrue(getContext(result, ResourceType.TOPIC).getActions().containsAll(Arrays.asList(Action.SUB))); + + request = ForwardMessageToDeadLetterQueueRequest.newBuilder() + .setTopic(Resource.newBuilder().setName("topic").build()) + .setGroup(Resource.newBuilder().setName("group").build()) + .build(); + result = builder.build(metadata, request); + Assert.assertEquals(2, result.size()); + Assert.assertEquals(getContext(result, ResourceType.GROUP).getSubject().getSubjectKey(), "User:rocketmq"); + Assert.assertEquals(getContext(result, ResourceType.GROUP).getResource().getResourceKey(), "Group:group"); + Assert.assertTrue(getContext(result, ResourceType.GROUP).getActions().containsAll(Arrays.asList(Action.SUB))); + Assert.assertEquals(getContext(result, ResourceType.TOPIC).getSubject().getSubjectKey(), "User:rocketmq"); + Assert.assertEquals(getContext(result, ResourceType.TOPIC).getResource().getResourceKey(), "Topic:topic"); + Assert.assertTrue(getContext(result, ResourceType.TOPIC).getActions().containsAll(Arrays.asList(Action.SUB))); + + request = NotifyClientTerminationRequest.newBuilder() + .setGroup(Resource.newBuilder().setName("group").build()) + .build(); + result = builder.build(metadata, request); + Assert.assertEquals(1, result.size()); + Assert.assertEquals(result.get(0).getSubject().getSubjectKey(), "User:rocketmq"); + Assert.assertEquals(result.get(0).getResource().getResourceKey(), "Group:group"); + Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.SUB))); + + request = ChangeInvisibleDurationRequest.newBuilder() + .setGroup(Resource.newBuilder().setName("group").build()) + .build(); + result = builder.build(metadata, request); + Assert.assertEquals(1, result.size()); + Assert.assertEquals(result.get(0).getSubject().getSubjectKey(), "User:rocketmq"); + Assert.assertEquals(result.get(0).getResource().getResourceKey(), "Group:group"); + Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.SUB))); + + request = QueryRouteRequest.newBuilder() + .setTopic(Resource.newBuilder().setName("topic").build()) + .build(); + result = builder.build(metadata, request); + Assert.assertEquals(1, result.size()); + Assert.assertEquals(result.get(0).getSubject().getSubjectKey(), "User:rocketmq"); + Assert.assertEquals(result.get(0).getResource().getResourceKey(), "Topic:topic"); + Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.PUB, Action.SUB))); + + request = QueryAssignmentRequest.newBuilder() + .setTopic(Resource.newBuilder().setName("topic").build()) + .setGroup(Resource.newBuilder().setName("group").build()) + .build(); + result = builder.build(metadata, request); + Assert.assertEquals(2, result.size()); + Assert.assertEquals(getContext(result, ResourceType.GROUP).getSubject().getSubjectKey(), "User:rocketmq"); + Assert.assertEquals(getContext(result, ResourceType.GROUP).getResource().getResourceKey(), "Group:group"); + Assert.assertTrue(getContext(result, ResourceType.GROUP).getActions().containsAll(Arrays.asList(Action.SUB))); + Assert.assertEquals(getContext(result, ResourceType.TOPIC).getSubject().getSubjectKey(), "User:rocketmq"); + Assert.assertEquals(getContext(result, ResourceType.TOPIC).getResource().getResourceKey(), "Topic:topic"); + Assert.assertTrue(getContext(result, ResourceType.TOPIC).getActions().containsAll(Arrays.asList(Action.SUB))); + + request = TelemetryCommand.newBuilder() + .setSettings(Settings.newBuilder() + .setPublishing(Publishing.newBuilder() + .addTopics(Resource.newBuilder().setName("topic").build()) + .build()) + .build()) + .build(); + result = builder.build(metadata, request); + Assert.assertEquals(1, result.size()); + Assert.assertEquals(getContext(result, ResourceType.TOPIC).getSubject().getSubjectKey(), "User:rocketmq"); + Assert.assertEquals(getContext(result, ResourceType.TOPIC).getResource().getResourceKey(), "Topic:topic"); + Assert.assertTrue(getContext(result, ResourceType.TOPIC).getActions().containsAll(Arrays.asList(Action.PUB))); + + request = TelemetryCommand.newBuilder() + .setSettings(Settings.newBuilder() + .setSubscription(Subscription.newBuilder() + .setGroup(Resource.newBuilder().setName("group").build()) + .addSubscriptions(SubscriptionEntry.newBuilder() + .setTopic(Resource.newBuilder().setName("topic").build()) + .build()) + .build()) + .build()) + .build(); + result = builder.build(metadata, request); + Assert.assertEquals(2, result.size()); + Assert.assertEquals(getContext(result, ResourceType.GROUP).getSubject().getSubjectKey(), "User:rocketmq"); + Assert.assertEquals(getContext(result, ResourceType.GROUP).getResource().getResourceKey(), "Group:group"); + Assert.assertTrue(getContext(result, ResourceType.GROUP).getActions().containsAll(Arrays.asList(Action.SUB))); + Assert.assertEquals(getContext(result, ResourceType.TOPIC).getSubject().getSubjectKey(), "User:rocketmq"); + Assert.assertEquals(getContext(result, ResourceType.TOPIC).getResource().getResourceKey(), "Topic:topic"); + Assert.assertTrue(getContext(result, ResourceType.TOPIC).getActions().containsAll(Arrays.asList(Action.SUB))); + } + + @Test + public void buildRemoting() { + when(channel.id()).thenReturn(mockChannelId("channel-id")); + when(channel.hasAttr(eq(AttributeKeys.PROXY_PROTOCOL_ADDR))).thenReturn(true); + when(channel.attr(eq(AttributeKeys.PROXY_PROTOCOL_ADDR))).thenReturn(mockAttribute("192.168.0.1")); + when(channel.hasAttr(eq(AttributeKeys.PROXY_PROTOCOL_PORT))).thenReturn(true); + when(channel.attr(eq(AttributeKeys.PROXY_PROTOCOL_PORT))).thenReturn(mockAttribute("1234")); + when(channelHandlerContext.channel()).thenReturn(channel); + + SendMessageRequestHeader sendMessageRequestHeader = new SendMessageRequestHeader(); + sendMessageRequestHeader.setTopic("topic"); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, sendMessageRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + List result = builder.build(channelHandlerContext, request); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("User:rocketmq", result.get(0).getSubject().getSubjectKey()); + Assert.assertEquals("Topic:topic", result.get(0).getResource().getResourceKey()); + Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.PUB))); + Assert.assertEquals("192.168.0.1", result.get(0).getSourceIp()); + Assert.assertEquals("channel-id", result.get(0).getChannelId()); + Assert.assertEquals(RequestCode.SEND_MESSAGE + "", result.get(0).getRpcCode()); + + sendMessageRequestHeader = new SendMessageRequestHeader(); + sendMessageRequestHeader.setTopic("%RETRY%group"); + request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, sendMessageRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + result = builder.build(channelHandlerContext, request); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("User:rocketmq", result.get(0).getSubject().getSubjectKey()); + Assert.assertEquals("Group:group", result.get(0).getResource().getResourceKey()); + Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.SUB))); + + SendMessageRequestHeaderV2 sendMessageRequestHeaderV2 = new SendMessageRequestHeaderV2(); + sendMessageRequestHeaderV2.setTopic("topic"); + request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE_V2, sendMessageRequestHeaderV2); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + result = builder.build(channelHandlerContext, request); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("User:rocketmq", result.get(0).getSubject().getSubjectKey()); + Assert.assertEquals("Topic:topic", result.get(0).getResource().getResourceKey()); + Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.PUB))); + + sendMessageRequestHeaderV2 = new SendMessageRequestHeaderV2(); + sendMessageRequestHeaderV2.setTopic("%RETRY%group"); + request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE_V2, sendMessageRequestHeaderV2); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + result = builder.build(channelHandlerContext, request); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("User:rocketmq", result.get(0).getSubject().getSubjectKey()); + Assert.assertEquals("Group:group", result.get(0).getResource().getResourceKey()); + Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.SUB))); + + RecallMessageRequestHeader recallMessageRequestHeader = new RecallMessageRequestHeader(); + recallMessageRequestHeader.setTopic("topic"); + recallMessageRequestHeader.setRecallHandle("handle"); + request = RemotingCommand.createRequestCommand(RequestCode.RECALL_MESSAGE, recallMessageRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + result = builder.build(channelHandlerContext, request); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("User:rocketmq", result.get(0).getSubject().getSubjectKey()); + Assert.assertEquals("Topic:topic", result.get(0).getResource().getResourceKey()); + Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.PUB))); + Assert.assertEquals("192.168.0.1", result.get(0).getSourceIp()); + Assert.assertEquals("channel-id", result.get(0).getChannelId()); + Assert.assertEquals(RequestCode.RECALL_MESSAGE + "", result.get(0).getRpcCode()); + + EndTransactionRequestHeader endTransactionRequestHeader = new EndTransactionRequestHeader(); + endTransactionRequestHeader.setTopic("topic"); + request = RemotingCommand.createRequestCommand(RequestCode.END_TRANSACTION, endTransactionRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + result = builder.build(channelHandlerContext, request); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("User:rocketmq", result.get(0).getSubject().getSubjectKey()); + Assert.assertEquals("Topic:topic", result.get(0).getResource().getResourceKey()); + Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.PUB))); + + endTransactionRequestHeader = new EndTransactionRequestHeader(); + request = RemotingCommand.createRequestCommand(RequestCode.END_TRANSACTION, endTransactionRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + result = builder.build(channelHandlerContext, request); + Assert.assertEquals(0, result.size()); + + ConsumerSendMsgBackRequestHeader consumerSendMsgBackRequestHeader = new ConsumerSendMsgBackRequestHeader(); + consumerSendMsgBackRequestHeader.setGroup("group"); + request = RemotingCommand.createRequestCommand(RequestCode.CONSUMER_SEND_MSG_BACK, consumerSendMsgBackRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + result = builder.build(channelHandlerContext, request); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("User:rocketmq", result.get(0).getSubject().getSubjectKey()); + Assert.assertEquals("Group:group", result.get(0).getResource().getResourceKey()); + Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.SUB))); + + PullMessageRequestHeader pullMessageRequestHeader = new PullMessageRequestHeader(); + pullMessageRequestHeader.setTopic("topic"); + pullMessageRequestHeader.setConsumerGroup("group"); + request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, pullMessageRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + result = builder.build(channelHandlerContext, request); + Assert.assertEquals(2, result.size()); + Assert.assertEquals("User:rocketmq", getContext(result, ResourceType.GROUP).getSubject().getSubjectKey()); + Assert.assertEquals("Group:group", getContext(result, ResourceType.GROUP).getResource().getResourceKey()); + Assert.assertTrue(getContext(result, ResourceType.GROUP).getActions().containsAll(Arrays.asList(Action.SUB))); + Assert.assertEquals("User:rocketmq", getContext(result, ResourceType.TOPIC).getSubject().getSubjectKey()); + Assert.assertEquals("Topic:topic", getContext(result, ResourceType.TOPIC).getResource().getResourceKey()); + Assert.assertTrue(getContext(result, ResourceType.TOPIC).getActions().containsAll(Arrays.asList(Action.SUB))); + + QueryMessageRequestHeader queryMessageRequestHeader = new QueryMessageRequestHeader(); + queryMessageRequestHeader.setTopic("topic"); + request = RemotingCommand.createRequestCommand(RequestCode.QUERY_MESSAGE, queryMessageRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + result = builder.build(channelHandlerContext, request); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("User:rocketmq", result.get(0).getSubject().getSubjectKey()); + Assert.assertEquals("Topic:topic", result.get(0).getResource().getResourceKey()); + Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.SUB, Action.GET))); + + HeartbeatRequestHeader heartbeatRequestHeader = new HeartbeatRequestHeader(); + request = RemotingCommand.createRequestCommand(RequestCode.HEART_BEAT, heartbeatRequestHeader); + HeartbeatData heartbeatData = new HeartbeatData(); + ConsumerData consumerData = new ConsumerData(); + consumerData.setGroupName("group"); + SubscriptionData subscriptionData = new SubscriptionData(); + subscriptionData.setTopic("topic"); + consumerData.setSubscriptionDataSet(Sets.newHashSet(subscriptionData)); + heartbeatData.setConsumerDataSet(Sets.newHashSet(consumerData)); + request.setBody(JSON.toJSONBytes(heartbeatData)); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + result = builder.build(channelHandlerContext, request); + Assert.assertEquals(2, result.size()); + Assert.assertEquals("User:rocketmq", getContext(result, ResourceType.GROUP).getSubject().getSubjectKey()); + Assert.assertEquals("Group:group", getContext(result, ResourceType.GROUP).getResource().getResourceKey()); + Assert.assertTrue(getContext(result, ResourceType.GROUP).getActions().containsAll(Arrays.asList(Action.SUB))); + Assert.assertEquals("User:rocketmq", getContext(result, ResourceType.TOPIC).getSubject().getSubjectKey()); + Assert.assertEquals("Topic:topic", getContext(result, ResourceType.TOPIC).getResource().getResourceKey()); + Assert.assertTrue(getContext(result, ResourceType.TOPIC).getActions().containsAll(Arrays.asList(Action.SUB))); + + UnregisterClientRequestHeader unregisterClientRequestHeader = new UnregisterClientRequestHeader(); + unregisterClientRequestHeader.setConsumerGroup("group"); + request = RemotingCommand.createRequestCommand(RequestCode.UNREGISTER_CLIENT, unregisterClientRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + result = builder.build(channelHandlerContext, request); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("User:rocketmq", result.get(0).getSubject().getSubjectKey()); + Assert.assertEquals("Group:group", result.get(0).getResource().getResourceKey()); + Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.SUB))); + + GetConsumerListByGroupRequestHeader getConsumerListByGroupRequestHeader = new GetConsumerListByGroupRequestHeader(); + getConsumerListByGroupRequestHeader.setConsumerGroup("group"); + request = RemotingCommand.createRequestCommand(RequestCode.GET_CONSUMER_LIST_BY_GROUP, getConsumerListByGroupRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + result = builder.build(channelHandlerContext, request); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("User:rocketmq", result.get(0).getSubject().getSubjectKey()); + Assert.assertEquals("Group:group", result.get(0).getResource().getResourceKey()); + Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.SUB, Action.GET))); + + QueryConsumerOffsetRequestHeader queryConsumerOffsetRequestHeader = new QueryConsumerOffsetRequestHeader(); + queryConsumerOffsetRequestHeader.setTopic("topic"); + queryConsumerOffsetRequestHeader.setConsumerGroup("group"); + request = RemotingCommand.createRequestCommand(RequestCode.QUERY_CONSUMER_OFFSET, queryConsumerOffsetRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + result = builder.build(channelHandlerContext, request); + Assert.assertEquals(2, result.size()); + Assert.assertEquals("User:rocketmq", getContext(result, ResourceType.GROUP).getSubject().getSubjectKey()); + Assert.assertEquals("Group:group", getContext(result, ResourceType.GROUP).getResource().getResourceKey()); + Assert.assertTrue(getContext(result, ResourceType.GROUP).getActions().containsAll(Arrays.asList(Action.SUB))); + Assert.assertEquals("User:rocketmq", getContext(result, ResourceType.TOPIC).getSubject().getSubjectKey()); + Assert.assertEquals("Topic:topic", getContext(result, ResourceType.TOPIC).getResource().getResourceKey()); + Assert.assertTrue(getContext(result, ResourceType.TOPIC).getActions().containsAll(Arrays.asList(Action.SUB))); + + UpdateConsumerOffsetRequestHeader updateConsumerOffsetRequestHeader = new UpdateConsumerOffsetRequestHeader(); + updateConsumerOffsetRequestHeader.setTopic("topic"); + updateConsumerOffsetRequestHeader.setConsumerGroup("group"); + request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_CONSUMER_OFFSET, updateConsumerOffsetRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + result = builder.build(channelHandlerContext, request); + Assert.assertEquals(2, result.size()); + Assert.assertEquals("User:rocketmq", getContext(result, ResourceType.GROUP).getSubject().getSubjectKey()); + Assert.assertEquals("Group:group", getContext(result, ResourceType.GROUP).getResource().getResourceKey()); + Assert.assertTrue(getContext(result, ResourceType.GROUP).getActions().containsAll(Arrays.asList(Action.SUB, Action.UPDATE))); + Assert.assertEquals("User:rocketmq", getContext(result, ResourceType.TOPIC).getSubject().getSubjectKey()); + Assert.assertEquals("Topic:topic", getContext(result, ResourceType.TOPIC).getResource().getResourceKey()); + Assert.assertTrue(getContext(result, ResourceType.TOPIC).getActions().containsAll(Arrays.asList(Action.SUB, Action.UPDATE))); + + CreateTopicRequestHeader createTopicRequestHeader = new CreateTopicRequestHeader(); + createTopicRequestHeader.setTopic("topic"); + createTopicRequestHeader.setTopicFilterType(TopicFilterType.SINGLE_TAG.name()); + request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_AND_CREATE_TOPIC, createTopicRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + result = builder.build(channelHandlerContext, request); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("User:rocketmq", result.get(0).getSubject().getSubjectKey()); + Assert.assertEquals("Topic:topic", result.get(0).getResource().getResourceKey()); + Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.CREATE))); + + CreateUserRequestHeader createUserRequestHeader = new CreateUserRequestHeader(); + createUserRequestHeader.setUsername("abc"); + request = RemotingCommand.createRequestCommand(RequestCode.AUTH_CREATE_USER, createUserRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + result = builder.build(channelHandlerContext, request); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("User:rocketmq", result.get(0).getSubject().getSubjectKey()); + Assert.assertEquals("Cluster:DefaultCluster", result.get(0).getResource().getResourceKey()); + Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.UPDATE))); + } + + private DefaultAuthorizationContext getContext(List contexts, + ResourceType resourceType) { + return contexts.stream().filter(context -> context.getResource().getResourceType() == resourceType) + .findFirst().orElse(null); + } + + private ChannelId mockChannelId(String channelId) { + return new ChannelId() { + @Override + public String asShortText() { + return channelId; + } + + @Override + public String asLongText() { + return channelId; + } + + @Override + public int compareTo(ChannelId o) { + return 0; + } + }; + } + + private Attribute mockAttribute(String value) { + return new Attribute() { + @Override + public AttributeKey key() { + return null; + } + + @Override + public String get() { + return value; + } + + @Override + public void set(String value) { + } + + @Override + public String getAndSet(String value) { + return null; + } + + @Override + public String setIfAbsent(String value) { + return null; + } + + @Override + public String getAndRemove() { + return null; + } + + @Override + public boolean compareAndSet(String oldValue, String newValue) { + return false; + } + + @Override + public void remove() { + + } + }; + } +} \ No newline at end of file diff --git a/auth/src/test/java/org/apache/rocketmq/auth/authorization/manager/AuthorizationMetadataManagerTest.java b/auth/src/test/java/org/apache/rocketmq/auth/authorization/manager/AuthorizationMetadataManagerTest.java new file mode 100644 index 0000000..21ae30a --- /dev/null +++ b/auth/src/test/java/org/apache/rocketmq/auth/authorization/manager/AuthorizationMetadataManagerTest.java @@ -0,0 +1,262 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.auth.authorization.manager; + +import java.util.List; +import org.apache.commons.collections.CollectionUtils; +import org.apache.rocketmq.auth.authentication.factory.AuthenticationFactory; +import org.apache.rocketmq.auth.authentication.manager.AuthenticationMetadataManager; +import org.apache.rocketmq.auth.authentication.model.Subject; +import org.apache.rocketmq.auth.authentication.model.User; +import org.apache.rocketmq.auth.authorization.enums.Decision; +import org.apache.rocketmq.auth.authorization.enums.PolicyType; +import org.apache.rocketmq.auth.authorization.exception.AuthorizationException; +import org.apache.rocketmq.auth.authorization.factory.AuthorizationFactory; +import org.apache.rocketmq.auth.authorization.model.Acl; +import org.apache.rocketmq.auth.authorization.model.Policy; +import org.apache.rocketmq.auth.authorization.model.Resource; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.auth.helper.AuthTestHelper; +import org.apache.rocketmq.common.MixAll; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class AuthorizationMetadataManagerTest { + + private AuthConfig authConfig; + + private AuthenticationMetadataManager authenticationMetadataManager; + + private AuthorizationMetadataManager authorizationMetadataManager; + + @Before + public void setUp() throws Exception { + if (MixAll.isMac()) { + return; + } + this.authConfig = AuthTestHelper.createDefaultConfig(); + this.authenticationMetadataManager = AuthenticationFactory.getMetadataManager(this.authConfig); + this.authorizationMetadataManager = AuthorizationFactory.getMetadataManager(this.authConfig); + this.clearAllAcls(); + this.clearAllUsers(); + } + + @After + public void tearDown() throws Exception { + if (MixAll.isMac()) { + return; + } + this.clearAllAcls(); + this.clearAllUsers(); + this.authenticationMetadataManager.shutdown(); + this.authorizationMetadataManager.shutdown(); + } + + @Test + public void createAcl() { + if (MixAll.isMac()) { + return; + } + User user = User.of("test", "test"); + this.authenticationMetadataManager.createUser(user).join(); + + Acl acl1 = AuthTestHelper.buildAcl("User:test", "Topic:test,Group:test", "PUB,SUB", + "192.168.0.0/24,10.10.0.0/24", Decision.ALLOW); + this.authorizationMetadataManager.createAcl(acl1).join(); + Acl acl2 = this.authorizationMetadataManager.getAcl(Subject.of("User:test")).join(); + Assert.assertTrue(AuthTestHelper.isEquals(acl1, acl2)); + + user = User.of("abc", "abc"); + this.authenticationMetadataManager.createUser(user).join(); + + acl1 = AuthTestHelper.buildAcl("User:abc", PolicyType.DEFAULT, "Topic:*,Group:*", "PUB,SUB", + null, Decision.DENY); + this.authorizationMetadataManager.createAcl(acl1).join(); + acl2 = this.authorizationMetadataManager.getAcl(Subject.of("User:abc")).join(); + Assert.assertTrue(AuthTestHelper.isEquals(acl1, acl2)); + + Acl acl3 = AuthTestHelper.buildAcl("User:test", "Topic:test,Group:test", "PUB,SUB", + "192.168.0.0/24,10.10.0.0/24", Decision.ALLOW); + this.authorizationMetadataManager.createAcl(acl3).join(); + Acl acl4 = this.authorizationMetadataManager.getAcl(Subject.of("User:test")).join(); + Assert.assertTrue(AuthTestHelper.isEquals(acl3, acl4)); + + Assert.assertThrows(AuthorizationException.class, () -> { + try { + Acl acl5 = AuthTestHelper.buildAcl("User:ddd", "Topic:test,Group:test", "PUB,SUB", + "192.168.0.0/24,10.10.0.0/24", Decision.ALLOW); + this.authorizationMetadataManager.createAcl(acl5).join(); + } catch (Exception e) { + AuthTestHelper.handleException(e); + } + }); + } + + @Test + public void updateAcl() { + if (MixAll.isMac()) { + return; + } + User user = User.of("test", "test"); + this.authenticationMetadataManager.createUser(user).join(); + + Acl acl1 = AuthTestHelper.buildAcl("User:test", "Topic:test,Group:test", "PUB,SUB", + "192.168.0.0/24,10.10.0.0/24", Decision.ALLOW); + this.authorizationMetadataManager.createAcl(acl1).join(); + + Acl acl2 = AuthTestHelper.buildAcl("User:test", "Topic:abc,Group:abc", "PUB,SUB", + "192.168.0.0/24,10.10.0.0/24", Decision.ALLOW); + this.authorizationMetadataManager.updateAcl(acl2).join(); + + Acl acl3 = AuthTestHelper.buildAcl("User:test", "Topic:test,Group:test,Topic:abc,Group:abc", "PUB,SUB", + "192.168.0.0/24,10.10.0.0/24", Decision.ALLOW); + Acl acl4 = this.authorizationMetadataManager.getAcl(Subject.of("User:test")).join(); + Assert.assertTrue(AuthTestHelper.isEquals(acl3, acl4)); + + Policy policy = AuthTestHelper.buildPolicy("Topic:test,Group:test", "PUB,SUB,Create", "192.168.0.0/24", Decision.DENY); + acl4.updatePolicy(policy); + this.authorizationMetadataManager.updateAcl(acl4); + Acl acl5 = this.authorizationMetadataManager.getAcl(Subject.of("User:test")).join(); + Assert.assertTrue(AuthTestHelper.isEquals(acl4, acl5)); + + User user2 = User.of("abc", "abc"); + this.authenticationMetadataManager.createUser(user2).join(); + Acl acl6 = AuthTestHelper.buildAcl("User:abc", "Topic:test,Group:test", "PUB,SUB", + "192.168.0.0/24,10.10.0.0/24", Decision.ALLOW); + this.authorizationMetadataManager.updateAcl(acl6).join(); + Acl acl7 = this.authorizationMetadataManager.getAcl(Subject.of("User:abc")).join(); + Assert.assertTrue(AuthTestHelper.isEquals(acl6, acl7)); + } + + @Test + public void deleteAcl() { + if (MixAll.isMac()) { + return; + } + User user = User.of("test", "test"); + this.authenticationMetadataManager.createUser(user).join(); + + Acl acl1 = AuthTestHelper.buildAcl("User:test", "Topic:test,Group:test", "PUB,SUB", + "192.168.0.0/24,10.10.0.0/24", Decision.ALLOW); + this.authorizationMetadataManager.createAcl(acl1).join(); + + this.authorizationMetadataManager.deleteAcl(Subject.of("User:test"), PolicyType.CUSTOM, Resource.ofTopic("abc")).join(); + Acl acl2 = this.authorizationMetadataManager.getAcl(Subject.of("User:test")).join(); + Assert.assertTrue(AuthTestHelper.isEquals(acl1, acl2)); + + this.authorizationMetadataManager.deleteAcl(Subject.of("User:test"), PolicyType.CUSTOM, Resource.ofTopic("test")).join(); + Acl acl3 = AuthTestHelper.buildAcl("User:test", "Group:test", "PUB,SUB", + "192.168.0.0/24,10.10.0.0/24", Decision.ALLOW); + Acl acl4 = this.authorizationMetadataManager.getAcl(Subject.of("User:test")).join(); + Assert.assertTrue(AuthTestHelper.isEquals(acl3, acl4)); + + this.authorizationMetadataManager.deleteAcl(Subject.of("User:test")); + Acl acl5 = this.authorizationMetadataManager.getAcl(Subject.of("User:test")).join(); + Assert.assertNull(acl5); + + Assert.assertThrows(AuthorizationException.class, () -> { + try { + this.authorizationMetadataManager.deleteAcl(Subject.of("User:abc")).join(); + } catch (Exception e) { + AuthTestHelper.handleException(e); + } + }); + } + + @Test + public void getAcl() { + if (MixAll.isMac()) { + return; + } + User user = User.of("test", "test"); + this.authenticationMetadataManager.createUser(user).join(); + + Acl acl1 = AuthTestHelper.buildAcl("User:test", "Topic:test,Group:test", "PUB,SUB", + "192.168.0.0/24,10.10.0.0/24", Decision.ALLOW); + this.authorizationMetadataManager.createAcl(acl1).join(); + Acl acl2 = this.authorizationMetadataManager.getAcl(Subject.of("User:test")).join(); + Assert.assertTrue(AuthTestHelper.isEquals(acl1, acl2)); + + Assert.assertThrows(AuthorizationException.class, () -> { + try { + this.authorizationMetadataManager.getAcl(Subject.of("User:abc")).join(); + } catch (Exception e) { + AuthTestHelper.handleException(e); + } + }); + } + + @Test + public void listAcl() { + if (MixAll.isMac()) { + return; + } + User user1 = User.of("test-1", "test-1"); + this.authenticationMetadataManager.createUser(user1).join(); + User user2 = User.of("test-2", "test-2"); + this.authenticationMetadataManager.createUser(user2).join(); + + Acl acl1 = AuthTestHelper.buildAcl("User:test-1", "Topic:test-1,Group:test-1", "PUB,SUB", + "192.168.0.0/24,10.10.0.0/24", Decision.ALLOW); + this.authorizationMetadataManager.createAcl(acl1).join(); + + Acl acl2 = AuthTestHelper.buildAcl("User:test-2", "Topic:test-2,Group:test-2", "PUB,SUB", + "192.168.0.0/24,10.10.0.0/24", Decision.ALLOW); + this.authorizationMetadataManager.createAcl(acl2).join(); + + List acls1 = this.authorizationMetadataManager.listAcl(null, null).join(); + Assert.assertEquals(acls1.size(), 2); + + List acls2 = this.authorizationMetadataManager.listAcl("User:test-1", null).join(); + Assert.assertEquals(acls2.size(), 1); + + List acls3 = this.authorizationMetadataManager.listAcl("test", null).join(); + Assert.assertEquals(acls3.size(), 2); + + List acls4 = this.authorizationMetadataManager.listAcl(null, "Topic:test-1").join(); + Assert.assertEquals(acls4.size(), 1); + Assert.assertEquals(acls4.get(0).getPolicy(PolicyType.CUSTOM).getEntries().size(), 1); + + List acls5 = this.authorizationMetadataManager.listAcl(null, "test-1").join(); + Assert.assertEquals(acls5.size(), 1); + Assert.assertEquals(acls4.get(0).getPolicy(PolicyType.CUSTOM).getEntries().size(), 1); + + List acls6 = this.authorizationMetadataManager.listAcl("User:abc", null).join(); + Assert.assertTrue(CollectionUtils.isEmpty(acls6)); + + List acls7 = this.authorizationMetadataManager.listAcl(null, "Topic:abc").join(); + Assert.assertTrue(CollectionUtils.isEmpty(acls7)); + } + + private void clearAllUsers() { + List users = this.authenticationMetadataManager.listUser(null).join(); + if (CollectionUtils.isEmpty(users)) { + return; + } + users.forEach(user -> this.authenticationMetadataManager.deleteUser(user.getUsername()).join()); + } + + private void clearAllAcls() { + List acls = this.authorizationMetadataManager.listAcl(null, null).join(); + if (CollectionUtils.isEmpty(acls)) { + return; + } + acls.forEach(acl -> this.authorizationMetadataManager.deleteAcl(acl.getSubject(), null, null).join()); + } +} \ No newline at end of file diff --git a/auth/src/test/java/org/apache/rocketmq/auth/authorization/model/ResourceTest.java b/auth/src/test/java/org/apache/rocketmq/auth/authorization/model/ResourceTest.java new file mode 100644 index 0000000..a17a4ab --- /dev/null +++ b/auth/src/test/java/org/apache/rocketmq/auth/authorization/model/ResourceTest.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.auth.authorization.model; + +import org.apache.rocketmq.common.resource.ResourcePattern; +import org.apache.rocketmq.common.resource.ResourceType; +import org.junit.Assert; +import org.junit.Test; + +public class ResourceTest { + + @Test + public void parseResource() { + Resource resource = Resource.of("*"); + Assert.assertEquals(resource.getResourceType(), ResourceType.ANY); + Assert.assertNull(resource.getResourceName()); + Assert.assertEquals(resource.getResourcePattern(), ResourcePattern.ANY); + + resource = Resource.of("Topic:*"); + Assert.assertEquals(resource.getResourceType(), ResourceType.TOPIC); + Assert.assertNull(resource.getResourceName()); + Assert.assertEquals(resource.getResourcePattern(), ResourcePattern.ANY); + + resource = Resource.of("Topic:test-*"); + Assert.assertEquals(resource.getResourceType(), ResourceType.TOPIC); + Assert.assertEquals(resource.getResourceName(), "test-"); + Assert.assertEquals(resource.getResourcePattern(), ResourcePattern.PREFIXED); + + resource = Resource.of("Topic:test-1"); + Assert.assertEquals(resource.getResourceType(), ResourceType.TOPIC); + Assert.assertEquals(resource.getResourceName(), "test-1"); + Assert.assertEquals(resource.getResourcePattern(), ResourcePattern.LITERAL); + } + + @Test + public void isMatch() { + + } +} \ No newline at end of file diff --git a/auth/src/test/java/org/apache/rocketmq/auth/authorization/strategy/StatefulAuthorizationStrategyTest.java b/auth/src/test/java/org/apache/rocketmq/auth/authorization/strategy/StatefulAuthorizationStrategyTest.java new file mode 100644 index 0000000..80e1f0b --- /dev/null +++ b/auth/src/test/java/org/apache/rocketmq/auth/authorization/strategy/StatefulAuthorizationStrategyTest.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.auth.authorization.strategy; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import org.apache.commons.lang3.reflect.MethodUtils; +import org.apache.rocketmq.auth.authentication.model.Subject; +import org.apache.rocketmq.auth.authorization.context.AuthorizationContext; +import org.apache.rocketmq.auth.authorization.context.DefaultAuthorizationContext; +import org.apache.rocketmq.auth.authorization.exception.AuthorizationException; +import org.apache.rocketmq.auth.authorization.model.Resource; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.action.Action; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +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; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class StatefulAuthorizationStrategyTest { + + @Mock + private AuthConfig authConfig; + + private StatefulAuthorizationStrategy statefulAuthorizationStrategy; + + @Before + public void setUp() { + when(authConfig.getStatefulAuthorizationCacheExpiredSecond()).thenReturn(60); + when(authConfig.getStatefulAuthorizationCacheMaxNum()).thenReturn(100); + Supplier metadataService = mock(Supplier.class); + statefulAuthorizationStrategy = spy(new StatefulAuthorizationStrategy(authConfig, metadataService)); + } + + @Test + public void testEvaluateChannelIdBlankDoesNotUseCache() { + AuthorizationContext context = mock(AuthorizationContext.class); + when(context.getChannelId()).thenReturn(null); + statefulAuthorizationStrategy.evaluate(context); + verify(statefulAuthorizationStrategy, times(1)).doEvaluate(context); + } + + @Test + public void testEvaluateChannelIdNotNullCacheHit() throws InvocationTargetException, NoSuchMethodException, IllegalAccessException { + DefaultAuthorizationContext context = new DefaultAuthorizationContext(); + context.setChannelId("channelId"); + context.setSubject(Subject.of("User")); + context.setResource(Resource.of("Cluster")); + context.setActions(new ArrayList<>()); + context.setSourceIp("sourceIp"); + Pair pair = Pair.of(true, null); + Cache> authCache = Caffeine.newBuilder() + .expireAfterWrite(60, TimeUnit.SECONDS) + .maximumSize(100) + .build(); + authCache.put(buildKey(context), pair); + statefulAuthorizationStrategy.authCache = authCache; + statefulAuthorizationStrategy.evaluate(context); + verify(statefulAuthorizationStrategy, never()).doEvaluate(context); + } + + @Test + public void testEvaluateChannelIdNotNullCacheMiss() { + DefaultAuthorizationContext context = new DefaultAuthorizationContext(); + context.setChannelId("channelId"); + context.setSubject(Subject.of("User")); + context.setResource(Resource.of("Cluster")); + context.setActions(Collections.singletonList(Action.PUB)); + context.setSourceIp("sourceIp"); + statefulAuthorizationStrategy.authCache = Caffeine.newBuilder() + .expireAfterWrite(60, TimeUnit.SECONDS) + .maximumSize(100) + .build(); + statefulAuthorizationStrategy.evaluate(context); + verify(statefulAuthorizationStrategy, times(1)).doEvaluate(context); + } + + @Test + public void testEvaluateChannelIdNotNullCacheException() throws InvocationTargetException, NoSuchMethodException, IllegalAccessException { + DefaultAuthorizationContext context = new DefaultAuthorizationContext(); + context.setChannelId("channelId"); + context.setSubject(Subject.of("subjectKey")); + context.setResource(Resource.of("resourceKey")); + context.setActions(Collections.singletonList(Action.PUB)); + context.setSourceIp("sourceIp"); + AuthorizationException exception = new AuthorizationException("test"); + Pair pair = Pair.of(false, exception); + Cache> authCache = Caffeine.newBuilder() + .expireAfterWrite(60, TimeUnit.SECONDS) + .maximumSize(100) + .build(); + authCache.put(buildKey(context), pair); + statefulAuthorizationStrategy.authCache = authCache; + try { + statefulAuthorizationStrategy.evaluate(context); + fail("Expected AuthorizationException to be thrown"); + } catch (final AuthorizationException ex) { + assertEquals(exception, ex); + } + verify(statefulAuthorizationStrategy, never()).doEvaluate(context); + } + + private String buildKey(AuthorizationContext context) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException { + return (String) MethodUtils.invokeMethod(statefulAuthorizationStrategy, true, "buildKey", context); + } +} diff --git a/auth/src/test/java/org/apache/rocketmq/auth/helper/AuthTestHelper.java b/auth/src/test/java/org/apache/rocketmq/auth/helper/AuthTestHelper.java new file mode 100644 index 0000000..e31732a --- /dev/null +++ b/auth/src/test/java/org/apache/rocketmq/auth/helper/AuthTestHelper.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.auth.helper; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.auth.authentication.exception.AuthenticationException; +import org.apache.rocketmq.auth.authentication.model.Subject; +import org.apache.rocketmq.auth.authentication.provider.DefaultAuthenticationProvider; +import org.apache.rocketmq.auth.authentication.provider.LocalAuthenticationMetadataProvider; +import org.apache.rocketmq.auth.authorization.enums.Decision; +import org.apache.rocketmq.auth.authorization.enums.PolicyType; +import org.apache.rocketmq.auth.authorization.exception.AuthorizationException; +import org.apache.rocketmq.auth.authorization.model.Acl; +import org.apache.rocketmq.auth.authorization.model.Environment; +import org.apache.rocketmq.auth.authorization.model.Policy; +import org.apache.rocketmq.auth.authorization.model.PolicyEntry; +import org.apache.rocketmq.auth.authorization.model.Resource; +import org.apache.rocketmq.auth.authorization.provider.DefaultAuthorizationProvider; +import org.apache.rocketmq.auth.authorization.provider.LocalAuthorizationMetadataProvider; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.utils.ExceptionUtils; + +public class AuthTestHelper { + + public static AuthConfig createDefaultConfig() { + AuthConfig authConfig = new AuthConfig(); + authConfig.setConfigName("test-" + System.nanoTime()); + authConfig.setAuthConfigPath("~/config"); + authConfig.setAuthenticationEnabled(true); + authConfig.setAuthenticationProvider(DefaultAuthenticationProvider.class.getName()); + authConfig.setAuthenticationMetadataProvider(LocalAuthenticationMetadataProvider.class.getName()); + authConfig.setAuthorizationEnabled(true); + authConfig.setAuthorizationProvider(DefaultAuthorizationProvider.class.getName()); + authConfig.setAuthorizationMetadataProvider(LocalAuthorizationMetadataProvider.class.getName()); + return authConfig; + } + + public static Acl buildAcl(String subjectKey, String resources, String actions, String sourceIps, + Decision decision) { + return buildAcl(subjectKey, null, resources, actions, sourceIps, decision); + } + + public static Acl buildAcl(String subjectKey, PolicyType policyType, String resources, String actions, + String sourceIps, Decision decision) { + Subject subject = Subject.of(subjectKey); + Policy policy = buildPolicy(policyType, resources, actions, sourceIps, decision); + return Acl.of(subject, policy); + } + + public static Policy buildPolicy(String resources, String actions, String sourceIps, + Decision decision) { + return buildPolicy(null, resources, actions, sourceIps, decision); + } + + public static Policy buildPolicy(PolicyType policyType, String resources, String actions, String sourceIps, + Decision decision) { + List resourceList = Arrays.stream(StringUtils.split(resources, ",")) + .map(Resource::of).collect(Collectors.toList()); + List actionList = Arrays.stream(StringUtils.split(actions, ",")) + .map(Action::getByName).collect(Collectors.toList()); + Environment environment = null; + if (StringUtils.isNotBlank(sourceIps)) { + environment = Environment.of(Arrays.stream(StringUtils.split(sourceIps, ",")) + .collect(Collectors.toList())); + } + return Policy.of(policyType, resourceList, actionList, environment, decision); + } + + public static boolean isEquals(Acl acl1, Acl acl2) { + if (acl1 == null && acl2 == null) { + return true; + } + if (acl1 == null || acl2 == null) { + return false; + } + Subject subject1 = acl1.getSubject(); + Subject subject2 = acl2.getSubject(); + if (!isEquals(subject1, subject2)) { + return false; + } + Map policyMap1 = new HashMap<>(); + Map policyMap2 = new HashMap<>(); + if (CollectionUtils.isNotEmpty(acl1.getPolicies())) { + acl1.getPolicies().forEach(policy -> { + if (policy.getPolicyType() == null) { + policy.setPolicyType(PolicyType.CUSTOM); + } + policyMap1.put(policy.getPolicyType(), policy); + }); + } + if (CollectionUtils.isNotEmpty(acl2.getPolicies())) { + acl2.getPolicies().forEach(policy -> { + if (policy.getPolicyType() == null) { + policy.setPolicyType(PolicyType.CUSTOM); + } + policyMap2.put(policy.getPolicyType(), policy); + }); + } + if (policyMap1.size() != policyMap2.size()) { + return false; + } + Policy customPolicy1 = policyMap1.get(PolicyType.CUSTOM); + Policy customPolicy2 = policyMap2.get(PolicyType.CUSTOM); + if (!isEquals(customPolicy1, customPolicy2)) { + return false; + } + + Policy defaultPolicy1 = policyMap1.get(PolicyType.DEFAULT); + Policy defaultPolicy2 = policyMap2.get(PolicyType.DEFAULT); + if (!isEquals(defaultPolicy1, defaultPolicy2)) { + return false; + } + + return true; + } + + private static boolean isEquals(Policy policy1, Policy policy2) { + if (policy1 == null && policy2 == null) { + return true; + } + if (policy1 == null || policy2 == null) { + return false; + } + if (policy1.getPolicyType() != policy2.getPolicyType()) { + return false; + } + Map policyEntryMap1 = new HashMap<>(); + Map policyEntryMap2 = new HashMap<>(); + if (CollectionUtils.isNotEmpty(policy1.getEntries())) { + policy1.getEntries().forEach(policyEntry -> { + policyEntryMap1.put(policyEntry.getResource().getResourceKey(), policyEntry); + }); + } + if (CollectionUtils.isNotEmpty(policy2.getEntries())) { + policy2.getEntries().forEach(policyEntry -> { + policyEntryMap2.put(policyEntry.getResource().getResourceKey(), policyEntry); + }); + } + if (policyEntryMap1.size() != policyEntryMap2.size()) { + return false; + } + + for (String resourceKey : policyEntryMap1.keySet()) { + if (!isEquals(policyEntryMap1.get(resourceKey), policyEntryMap2.get(resourceKey))) { + return false; + } + } + + for (String resourceKey : policyEntryMap2.keySet()) { + if (!isEquals(policyEntryMap1.get(resourceKey), policyEntryMap2.get(resourceKey))) { + return false; + } + } + + return true; + } + + private static boolean isEquals(PolicyEntry entry1, PolicyEntry entry2) { + if (entry1 == null && entry2 == null) { + return true; + } + if (entry1 == null || entry2 == null) { + return false; + } + Resource resource1 = entry1.getResource(); + Resource resource2 = entry2.getResource(); + if (!isEquals(resource1, resource2)) { + return false; + } + List actions1 = entry1.getActions(); + List actions2 = entry2.getActions(); + if (CollectionUtils.isEmpty(actions1) && CollectionUtils.isNotEmpty(actions2)) { + return false; + } + if (CollectionUtils.isNotEmpty(actions1) && CollectionUtils.isEmpty(actions2)) { + return false; + } + if (CollectionUtils.isNotEmpty(actions1) && CollectionUtils.isNotEmpty(actions2) + && !CollectionUtils.isEqualCollection(actions1, actions2)) { + return false; + } + Environment environment1 = entry1.getEnvironment(); + Environment environment2 = entry2.getEnvironment(); + if (!isEquals(environment1, environment2)) { + return false; + } + return entry1.getDecision() == entry2.getDecision(); + } + + private static boolean isEquals(Resource resource1, Resource resource2) { + if (resource1 == null && resource2 == null) { + return true; + } + if (resource1 == null || resource2 == null) { + return false; + } + return Objects.equals(resource1, resource2); + } + + private static boolean isEquals(Environment environment1, Environment environment2) { + if (environment1 == null && environment2 == null) { + return true; + } + if (environment1 == null || environment2 == null) { + return false; + } + List sourceIp1 = environment1.getSourceIps(); + List sourceIp2 = environment2.getSourceIps(); + if (CollectionUtils.isEmpty(sourceIp1) && CollectionUtils.isEmpty(sourceIp2)) { + return true; + } + if (CollectionUtils.isEmpty(sourceIp1) || CollectionUtils.isEmpty(sourceIp2)) { + return false; + } + return CollectionUtils.isEqualCollection(sourceIp1, sourceIp2); + } + + private static boolean isEquals(Subject subject1, Subject subject2) { + if (subject1 == null && subject2 == null) { + return true; + } + if (subject1 == null || subject2 == null) { + return false; + } + return subject1.getSubjectType() == subject2.getSubjectType() + && StringUtils.equals(subject1.getSubjectKey(), subject2.getSubjectKey()); + } + + public static void handleException(Throwable e) { + Throwable throwable = ExceptionUtils.getRealException(e); + if (throwable instanceof AuthenticationException) { + throw (AuthenticationException) throwable; + } + if (throwable instanceof AuthorizationException) { + throw (AuthorizationException) throwable; + } + throw new RuntimeException(e); + } +} diff --git a/auth/src/test/java/org/apache/rocketmq/auth/migration/AuthMigratorTest.java b/auth/src/test/java/org/apache/rocketmq/auth/migration/AuthMigratorTest.java new file mode 100644 index 0000000..1b95051 --- /dev/null +++ b/auth/src/test/java/org/apache/rocketmq/auth/migration/AuthMigratorTest.java @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.auth.migration; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.auth.authentication.manager.AuthenticationMetadataManager; +import org.apache.rocketmq.auth.authentication.model.User; +import org.apache.rocketmq.auth.authorization.manager.AuthorizationMetadataManager; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.auth.migration.v1.PlainPermissionManager; +import org.apache.rocketmq.auth.migration.v1.AclConfig; +import org.apache.rocketmq.auth.migration.v1.PlainAccessConfig; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +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 AuthMigratorTest { + + @Mock + private AuthenticationMetadataManager authenticationMetadataManager; + + @Mock + private AuthorizationMetadataManager authorizationMetadataManager; + + @Mock + private PlainPermissionManager plainPermissionManager; + + @Mock + private AuthConfig authConfig; + + private AuthMigrator authMigrator; + + @Before + public void setUp() throws IllegalAccessException { + when(authConfig.isMigrateAuthFromV1Enabled()).thenReturn(true); + authMigrator = new AuthMigrator(authConfig); + FieldUtils.writeDeclaredField(authMigrator, "authenticationMetadataManager", authenticationMetadataManager, true); + FieldUtils.writeDeclaredField(authMigrator, "authorizationMetadataManager", authorizationMetadataManager, true); + FieldUtils.writeDeclaredField(authMigrator, "plainPermissionManager", plainPermissionManager, true); + } + + @Test + public void testMigrateNoAclConfigDoesNothing() { + AclConfig aclConfig = mock(AclConfig.class); + when(aclConfig.getPlainAccessConfigs()).thenReturn(new ArrayList<>()); + when(plainPermissionManager.getAllAclConfig()).thenReturn(aclConfig); + authMigrator.migrate(); + verify(authConfig, times(1)).isMigrateAuthFromV1Enabled(); + verify(plainPermissionManager, times(1)).getAllAclConfig(); + verify(authenticationMetadataManager, never()).createUser(any()); + verify(authorizationMetadataManager, never()).createAcl(any()); + } + + @Test + public void testMigrateWithAclConfigCreatesUserAndAcl() { + AclConfig aclConfig = mock(AclConfig.class); + List accessConfigs = new ArrayList<>(); + accessConfigs.add(createPlainAccessConfig()); + when(aclConfig.getPlainAccessConfigs()).thenReturn(accessConfigs); + when(plainPermissionManager.getAllAclConfig()).thenReturn(aclConfig); + when(authenticationMetadataManager.getUser(anyString())) + .thenReturn(CompletableFuture.completedFuture(null)); + when(authenticationMetadataManager.createUser(any())) + .thenReturn(CompletableFuture.completedFuture(null)); + authMigrator.migrate(); + verify(authConfig, times(1)).isMigrateAuthFromV1Enabled(); + verify(plainPermissionManager, times(1)).getAllAclConfig(); + verify(authenticationMetadataManager, times(1)).createUser(any()); + verify(authorizationMetadataManager, times(1)).createAcl(any()); + } + + @Test + public void testMigrateExceptionInMigrateLogsError() { + PlainAccessConfig accessConfig = mock(PlainAccessConfig.class); + when(accessConfig.getAccessKey()).thenReturn("testAk"); + when(authenticationMetadataManager.createUser(any(User.class))) + .thenThrow(new RuntimeException("Test Exception")); + AclConfig aclConfig = mock(AclConfig.class); + List accessConfigs = new ArrayList<>(); + accessConfigs.add(accessConfig); + when(aclConfig.getPlainAccessConfigs()).thenReturn(accessConfigs); + when(plainPermissionManager.getAllAclConfig()).thenReturn(aclConfig); + when(authenticationMetadataManager.getUser(anyString())) + .thenReturn(CompletableFuture.completedFuture(null)); + try { + authMigrator.migrate(); + verify(authConfig, times(1)).isMigrateAuthFromV1Enabled(); + verify(plainPermissionManager, times(1)).getAllAclConfig(); + verify(authenticationMetadataManager, times(1)).createUser(any()); + verify(authorizationMetadataManager, never()).createAcl(any()); + } catch (final RuntimeException ex) { + assertEquals("Test Exception", ex.getMessage()); + } + } + + private PlainAccessConfig createPlainAccessConfig() { + PlainAccessConfig result = mock(PlainAccessConfig.class); + when(result.getAccessKey()).thenReturn("testAk"); + when(result.getSecretKey()).thenReturn("testSk"); + when(result.isAdmin()).thenReturn(false); + when(result.getTopicPerms()).thenReturn(new ArrayList<>()); + when(result.getGroupPerms()).thenReturn(new ArrayList<>()); + when(result.getDefaultTopicPerm()).thenReturn("PUB"); + when(result.getDefaultGroupPerm()).thenReturn(null); + return result; + } +} diff --git a/bazel/BUILD.bazel b/bazel/BUILD.bazel new file mode 100644 index 0000000..a9fd83f --- /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 0000000..fb9b699 --- /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 0000000..9d61c0a --- /dev/null +++ b/broker/BUILD.bazel @@ -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. +# +load("//bazel:GenTestRules.bzl", "GenTestRules") + +java_library( + name = "broker", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//auth", + "//client", + "//common", + "//filter", + "//remoting", + "//srvutil", + "//store", + "//tieredstore", + "@maven//:org_slf4j_slf4j_api", + "@maven//:ch_qos_logback_logback_classic", + "@maven//:com_alibaba_fastjson", + "@maven//:com_alibaba_fastjson2_fastjson2", + "@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//:commons_codec_commons_codec", + "@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.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", + "//auth", + "//client", + "//common", + "//filter", + "//remoting", + "//store", + "//tieredstore", + "@maven//:com_alibaba_fastjson", + "@maven//:com_alibaba_fastjson2_fastjson2", + "@maven//:org_slf4j_slf4j_api", + "@maven//:com_google_guava_guava", + "@maven//:io_netty_netty_all", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:commons_codec_commons_codec", + "@maven//:commons_io_commons_io", + "@maven//:io_github_aliyunmq_rocketmq_slf4j_api", + "@maven//:org_powermock_powermock_core", + "@maven//:io_opentelemetry_opentelemetry_api", + "@maven//:com_googlecode_concurrentlinkedhashmap_concurrentlinkedhashmap_lru", + "@maven//:org_apache_rocketmq_rocketmq_rocksdb", + "@maven//:commons_collections_commons_collections", + "@maven//:org_junit_jupiter_junit_jupiter_api", + ], +) + +GenTestRules( + name = "GeneratedTestRules", + test_files = glob(["src/test/java/**/*Test.java"]), + exclude_tests = [ + # These tests are extremely slow and flaky, exclude them before they are properly fixed. + "src/test/java/org/apache/rocketmq/broker/controller/ReplicasManagerRegisterTest", + "src/test/java/org/apache/rocketmq/broker/BrokerOuterAPITest", + ], + deps = [ + ":tests", + ], +) diff --git a/broker/pom.xml b/broker/pom.xml new file mode 100644 index 0000000..fb879b4 --- /dev/null +++ b/broker/pom.xml @@ -0,0 +1,107 @@ + + + + + org.apache.rocketmq + rocketmq-all + 5.3.4-SNAPSHOT + + + 4.0.0 + jar + rocketmq-broker + rocketmq-broker ${project.version} + + + ${basedir}/.. + + + + + ${project.groupId} + rocketmq-remoting + + + ${project.groupId} + rocketmq-store + + + ${project.groupId} + rocketmq-tiered-store + + + io.github.aliyunmq + rocketmq-slf4j-api + + + io.github.aliyunmq + rocketmq-logback-classic + + + ${project.groupId} + rocketmq-client + + + ${project.groupId} + rocketmq-srvutil + + + ${project.groupId} + rocketmq-filter + + + org.apache.rocketmq + rocketmq-auth + + + commons-io + commons-io + + + com.alibaba + fastjson + + + 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 + ${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 new file mode 100644 index 0000000..5a9fa26 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/BrokerController.java @@ -0,0 +1,2618 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.annotations.VisibleForTesting; +import com.google.common.collect.Lists; +import org.apache.rocketmq.auth.authentication.factory.AuthenticationFactory; +import org.apache.rocketmq.auth.authentication.manager.AuthenticationMetadataManager; +import org.apache.rocketmq.auth.authorization.factory.AuthorizationFactory; +import org.apache.rocketmq.auth.authorization.manager.AuthorizationMetadataManager; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.auth.migration.AuthMigrator; +import org.apache.rocketmq.broker.auth.pipeline.AuthenticationPipeline; +import org.apache.rocketmq.broker.auth.pipeline.AuthorizationPipeline; +import org.apache.rocketmq.broker.client.ClientHousekeepingService; +import org.apache.rocketmq.broker.client.ConsumerIdsChangeListener; +import org.apache.rocketmq.broker.client.ConsumerManager; +import org.apache.rocketmq.broker.client.DefaultConsumerIdsChangeListener; +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.config.v1.RocksDBConsumerOffsetManager; +import org.apache.rocketmq.broker.config.v1.RocksDBLmqSubscriptionGroupManager; +import org.apache.rocketmq.broker.config.v1.RocksDBLmqTopicConfigManager; +import org.apache.rocketmq.broker.config.v1.RocksDBSubscriptionGroupManager; +import org.apache.rocketmq.broker.config.v1.RocksDBTopicConfigManager; +import org.apache.rocketmq.broker.config.v2.ConfigStorage; +import org.apache.rocketmq.broker.config.v2.ConsumerOffsetManagerV2; +import org.apache.rocketmq.broker.config.v2.SubscriptionGroupManagerV2; +import org.apache.rocketmq.broker.config.v2.TopicConfigManagerV2; +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.latency.BrokerFastFailure; +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.out.BrokerOuterAPI; +import org.apache.rocketmq.broker.plugin.BrokerAttachedPlugin; +import org.apache.rocketmq.broker.pop.PopConsumerService; +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.RecallMessageProcessor; +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.SubscriptionGroupManager; +import org.apache.rocketmq.broker.topic.LmqTopicConfigManager; +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.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.config.ConfigManagerVersion; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.constant.PermName; +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.pipeline.RequestPipeline; +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.RequestCode; +import org.apache.rocketmq.remoting.protocol.RequestHeaderRegistry; +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.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; + +import java.net.InetSocketAddress; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +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.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +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.function.Supplier; +import java.util.stream.Collectors; + +public class BrokerController { + 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); + protected static final int HA_ADDRESS_MIN_LENGTH = 6; + + protected final BrokerConfig brokerConfig; + private final NettyServerConfig nettyServerConfig; + private final NettyClientConfig nettyClientConfig; + protected final MessageStoreConfig messageStoreConfig; + private final AuthConfig authConfig; + protected ConsumerOffsetManager consumerOffsetManager; + protected final BroadcastOffsetManager broadcastOffsetManager; + protected final ConsumerManager consumerManager; + protected final ConsumerFilterManager consumerFilterManager; + protected final ConsumerOrderInfoManager consumerOrderInfoManager; + protected final PopInflightMessageCounter popInflightMessageCounter; + protected final PopConsumerService popConsumerService; + 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 RecallMessageProcessor recallMessageProcessor; + 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 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 BrokerStatsManager brokerStatsManager; + protected final List sendMessageHookList = new ArrayList<>(); + protected final List consumeMessageHookList = new ArrayList<>(); + protected MessageStore messageStore; + protected static final String TCP_REMOTING_SERVER = "TCP_REMOTING_SERVER"; + protected static final String FAST_REMOTING_SERVER = "FAST_REMOTING_SERVER"; + protected final Map remotingServerMap = new ConcurrentHashMap<>(); + protected CountDownLatch remotingServerStartLatch; + /** + * If {Topic, SubscriptionGroup, Offset}ManagerV2 are used, config entries are stored in RocksDB. + */ + protected ConfigStorage configStorage; + 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 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 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; + private AuthenticationMetadataManager authenticationMetadataManager; + private AuthorizationMetadataManager authorizationMetadataManager; + + public BrokerController( + final BrokerConfig brokerConfig, + final NettyServerConfig nettyServerConfig, + final NettyClientConfig nettyClientConfig, + final MessageStoreConfig messageStoreConfig, + final AuthConfig authConfig, + final ShutdownHook shutdownHook + ) { + this(brokerConfig, nettyServerConfig, nettyClientConfig, messageStoreConfig, authConfig); + this.shutdownHook = shutdownHook; + } + + public BrokerController( + final BrokerConfig brokerConfig, + final MessageStoreConfig messageStoreConfig + ) { + this(brokerConfig, null, null, messageStoreConfig, null); + } + + public BrokerController( + final BrokerConfig brokerConfig, + final NettyServerConfig nettyServerConfig, + final NettyClientConfig nettyClientConfig, + final MessageStoreConfig messageStoreConfig + ) { + this(brokerConfig, nettyServerConfig, nettyClientConfig, messageStoreConfig, null); + } + + public BrokerController( + final BrokerConfig brokerConfig, + final NettyServerConfig nettyServerConfig, + final NettyClientConfig nettyClientConfig, + final MessageStoreConfig messageStoreConfig, + final AuthConfig authConfig + ) { + this.brokerConfig = brokerConfig; + this.nettyServerConfig = nettyServerConfig; + this.nettyClientConfig = nettyClientConfig; + this.messageStoreConfig = messageStoreConfig; + this.authConfig = authConfig; + this.setStoreHost(new InetSocketAddress(this.getBrokerConfig().getBrokerIP1(), getListenPort())); + this.brokerStatsManager = messageStoreConfig.isEnableLmq() ? new LmqBrokerStatsManager(this.brokerConfig) : new BrokerStatsManager(this.brokerConfig.getBrokerClusterName(), this.brokerConfig.isEnableDetailStat()); + this.broadcastOffsetManager = new BroadcastOffsetManager(this); + if (ConfigManagerVersion.V2.getVersion().equals(brokerConfig.getConfigManagerVersion())) { + this.configStorage = new ConfigStorage(messageStoreConfig); + this.topicConfigManager = new TopicConfigManagerV2(this, configStorage); + this.subscriptionGroupManager = new SubscriptionGroupManagerV2(this, configStorage); + this.consumerOffsetManager = new ConsumerOffsetManagerV2(this, configStorage); + } else 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 = 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.authenticationMetadataManager = AuthenticationFactory.getMetadataManager(this.authConfig); + this.authorizationMetadataManager = AuthorizationFactory.getMetadataManager(this.authConfig); + this.pullMessageProcessor = new PullMessageProcessor(this); + 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.recallMessageProcessor = new RecallMessageProcessor(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.brokerStatsManager, this.brokerConfig); + this.producerManager = new ProducerManager(this.brokerStatsManager); + this.consumerFilterManager = new ConsumerFilterManager(this); + this.consumerOrderInfoManager = new ConsumerOrderInfoManager(this); + this.popInflightMessageCounter = new PopInflightMessageCounter(this); + this.popConsumerService = brokerConfig.isPopConsumerKVServiceInit() ? new PopConsumerService(this) : null; + this.clientHousekeepingService = new ClientHousekeepingService(this); + this.broker2Client = new Broker2Client(this); + this.scheduleMessageService = new ScheduleMessageService(this); + this.coldDataPullRequestHoldService = new ColdDataPullRequestHoldService(this); + this.coldDataCgCtrService = new ColdDataCgCtrService(this); + + if (nettyClientConfig != null) { + this.brokerOuterAPI = new BrokerOuterAPI(nettyClientConfig, authConfig); + } + + 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, + brokerConfigPath, + this.brokerConfig, this.nettyServerConfig, this.nettyClientConfig, this.messageStoreConfig + ); + + this.brokerStatsManager.setProducerStateGetter(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); + } + + if (this.authConfig != null && this.authConfig.isMigrateAuthFromV1Enabled()) { + new AuthMigrator(this.authConfig).migrate(); + } + } + + public AuthConfig getAuthConfig() { + return authConfig; + } + + public BrokerConfig getBrokerConfig() { + return brokerConfig; + } + + public NettyServerConfig getNettyServerConfig() { + return nettyServerConfig; + } + + public NettyClientConfig getNettyClientConfig() { + return nettyClientConfig; + } + + public BlockingQueue getPullThreadPoolQueue() { + return pullThreadPoolQueue; + } + + public BlockingQueue getQueryThreadPoolQueue() { + return queryThreadPoolQueue; + } + + public BrokerMetricsManager getBrokerMetricsManager() { + return brokerMetricsManager; + } + + protected void initializeRemotingServer() throws CloneNotSupportedException { + RemotingServer tcpRemotingServer = new NettyRemotingServer(this.nettyServerConfig, this.clientHousekeepingService); + NettyServerConfig fastConfig = (NettyServerConfig) this.nettyServerConfig.clone(); + + int listeningPort = nettyServerConfig.getListenPort() - 2; + if (listeningPort < 0) { + listeningPort = 0; + } + fastConfig.setListenPort(listeningPort); + + RemotingServer fastRemotingServer = new NettyRemotingServer(fastConfig, this.clientHousekeepingService); + + remotingServerMap.put(TCP_REMOTING_SERVER, tcpRemotingServer); + remotingServerMap.put(FAST_REMOTING_SERVER, fastRemotingServer); + } + + /** + * 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("PutMessageThread_", 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); + } + + 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); + } + } + }, initialDelay, period, TimeUnit.MILLISECONDS); + + 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, this.brokerConfig.getFlushConsumerOffsetInterval(), TimeUnit.MILLISECONDS); + + 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); + } + } + }, 1000 * 10, 1000 * 10, TimeUnit.MILLISECONDS); + + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + BrokerController.this.protectBroker(); + } catch (Throwable e) { + LOG.error("BrokerController: failed to protectBroker", e); + } + } + }, 3, 3, TimeUnit.MINUTES); + + 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); + } + } + }, 10, 1, TimeUnit.SECONDS); + + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + BrokerController.this.messageStore.getTimerMessageStore().getTimerMetrics() + .cleanMetrics(BrokerController.this.topicConfigManager.getTopicConfigTable().keySet()); + } catch (Throwable e) { + LOG.error("BrokerController: failed to clean unused timer metrics.", e); + } + } + }, 3, 3, TimeUnit.MINUTES); + + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + + @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() >= HA_ADDRESS_MIN_LENGTH) { + this.messageStore.updateHaMasterAddress(this.messageStoreConfig.getHaMasterAddress()); + this.updateMasterHAServerAddrPeriodically = false; + } else { + this.updateMasterHAServerAddrPeriodically = true; + } + + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + + @Override + public void run() { + try { + 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("Failed to sync all config for slave.", e); + } + } + }, 1000 * 10, 3 * 1000, TimeUnit.MILLISECONDS); + + } else { + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + + @Override + public void run() { + try { + BrokerController.this.printMasterAndSlaveDiff(); + } catch (Throwable e) { + LOG.error("Failed to print diff of master and slave.", e); + } + } + }, 1000 * 10, 1000 * 60, TimeUnit.MILLISECONDS); + } + } + + if (this.brokerConfig.isEnableControllerMode()) { + this.updateMasterHAServerAddrPeriodically = true; + } + } + + protected void initializeScheduledTasks() { + + initializeBrokerScheduledTasks(); + + 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, this.brokerConfig.getUpdateNameServerAddrPeriod(), TimeUnit.MILLISECONDS); + } else if (this.brokerConfig.isFetchNamesrvAddrByAddressServer()) { + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + + @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); + } + } + + private void updateNamesrvAddr() { + if (this.brokerConfig.isFetchNameSrvAddrByDnsLookup()) { + this.brokerOuterAPI.updateNameServerAddressListByDnsLookup(this.brokerConfig.getNamesrvAddr()); + } else { + this.brokerOuterAPI.updateNameServerAddressList(this.brokerConfig.getNamesrvAddr()); + } + } + + public boolean initializeMetadata() { + boolean result = true; + if (null != configStorage) { + result = configStorage.start(); + } + result = 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 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.isRocksdbCQDoubleWriteEnable()) { + defaultMessageStore.enableRocksdbCQWrite(); + } + } + + 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 (Exception e) { + result = false; + LOG.error("BrokerController#initialize: unexpected error occurs", e); + } + return result; + } + + public boolean initialize() throws CloneNotSupportedException { + + boolean result = this.initializeMetadata(); + if (!result) { + return false; + } + + result = this.initializeMessageStore(); + if (!result) { + return false; + } + + return this.recoverAndInitService(); + } + + public boolean recoverAndInitService() throws CloneNotSupportedException { + + boolean result = true; + + if (this.brokerConfig.isEnableControllerMode()) { + this.replicasManager = new ReplicasManager(this); + this.replicasManager.setFenced(true); + } + + if (messageStore != null) { + registerMessageStoreHook(); + result = this.messageStore.load(); + } + + if (messageStoreConfig.isTimerWheelEnable()) { + result = result && this.timerMessageStore.load(); + } + + //scheduleMessageService load after messageStore load success + result = result && this.scheduleMessageService.load(); + + for (BrokerAttachedPlugin brokerAttachedPlugin : brokerAttachedPlugins) { + if (brokerAttachedPlugin != null) { + result = result && brokerAttachedPlugin.load(); + } + } + + this.brokerMetricsManager = new BrokerMetricsManager(this); + + if (result) { + + initializeRemotingServer(); + + initializeResources(); + + registerProcessor(); + + initializeScheduledTasks(); + + initialTransaction(); + + initialRpcHooks(); + + initialRequestPipeline(); + + 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() { + for (Map.Entry entry : remotingServerMap.entrySet()) { + RemotingServer remotingServer = entry.getValue(); + if (remotingServer instanceof NettyRemotingServer) { + ((NettyRemotingServer) remotingServer).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 initialRpcHooks() { + + List rpcHooks = ServiceProvider.load(RPCHook.class); + if (rpcHooks == null || rpcHooks.isEmpty()) { + return; + } + for (RPCHook rpcHook : rpcHooks) { + this.registerServerRPCHook(rpcHook); + } + } + + private void initialRequestPipeline() { + if (this.authConfig == null) { + return; + } + RequestPipeline pipeline = (ctx, request) -> { + }; + // add pipeline + // the last pipe add will execute at the first + try { + pipeline = pipeline.pipe(new AuthorizationPipeline(authConfig)) + .pipe(new AuthenticationPipeline(authConfig)); + this.setRequestPipeline(pipeline); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public void registerProcessor() { + RemotingServer remotingServer = remotingServerMap.get(TCP_REMOTING_SERVER); + RemotingServer fastRemotingServer = remotingServerMap.get(FAST_REMOTING_SERVER); + + /* + * SendMessageProcessor + */ + sendMessageProcessor.registerSendMessageHook(sendMessageHookList); + sendMessageProcessor.registerConsumeMessageHook(consumeMessageHookList); + + remotingServer.registerProcessor(RequestCode.SEND_MESSAGE, sendMessageProcessor, this.sendMessageExecutor); + remotingServer.registerProcessor(RequestCode.SEND_MESSAGE_V2, sendMessageProcessor, this.sendMessageExecutor); + remotingServer.registerProcessor(RequestCode.SEND_BATCH_MESSAGE, sendMessageProcessor, this.sendMessageExecutor); + remotingServer.registerProcessor(RequestCode.CONSUMER_SEND_MSG_BACK, sendMessageProcessor, this.sendMessageExecutor); + remotingServer.registerProcessor(RequestCode.RECALL_MESSAGE, recallMessageProcessor, this.sendMessageExecutor); + fastRemotingServer.registerProcessor(RequestCode.SEND_MESSAGE, sendMessageProcessor, this.sendMessageExecutor); + fastRemotingServer.registerProcessor(RequestCode.SEND_MESSAGE_V2, sendMessageProcessor, this.sendMessageExecutor); + fastRemotingServer.registerProcessor(RequestCode.SEND_BATCH_MESSAGE, sendMessageProcessor, this.sendMessageExecutor); + fastRemotingServer.registerProcessor(RequestCode.CONSUMER_SEND_MSG_BACK, sendMessageProcessor, this.sendMessageExecutor); + fastRemotingServer.registerProcessor(RequestCode.RECALL_MESSAGE, recallMessageProcessor, this.sendMessageExecutor); + /** + * PullMessageProcessor + */ + remotingServer.registerProcessor(RequestCode.PULL_MESSAGE, this.pullMessageProcessor, this.pullMessageExecutor); + remotingServer.registerProcessor(RequestCode.LITE_PULL_MESSAGE, this.pullMessageProcessor, this.litePullMessageExecutor); + this.pullMessageProcessor.registerConsumeMessageHook(consumeMessageHookList); + /** + * PeekMessageProcessor + */ + remotingServer.registerProcessor(RequestCode.PEEK_MESSAGE, this.peekMessageProcessor, this.pullMessageExecutor); + /** + * PopMessageProcessor + */ + remotingServer.registerProcessor(RequestCode.POP_MESSAGE, this.popMessageProcessor, this.pullMessageExecutor); + + /** + * AckMessageProcessor + */ + remotingServer.registerProcessor(RequestCode.ACK_MESSAGE, this.ackMessageProcessor, this.ackMessageExecutor); + fastRemotingServer.registerProcessor(RequestCode.ACK_MESSAGE, this.ackMessageProcessor, this.ackMessageExecutor); + + remotingServer.registerProcessor(RequestCode.BATCH_ACK_MESSAGE, this.ackMessageProcessor, this.ackMessageExecutor); + fastRemotingServer.registerProcessor(RequestCode.BATCH_ACK_MESSAGE, this.ackMessageProcessor, this.ackMessageExecutor); + /** + * ChangeInvisibleTimeProcessor + */ + remotingServer.registerProcessor(RequestCode.CHANGE_MESSAGE_INVISIBLETIME, this.changeInvisibleTimeProcessor, this.ackMessageExecutor); + fastRemotingServer.registerProcessor(RequestCode.CHANGE_MESSAGE_INVISIBLETIME, this.changeInvisibleTimeProcessor, this.ackMessageExecutor); + /** + * notificationProcessor + */ + remotingServer.registerProcessor(RequestCode.NOTIFICATION, this.notificationProcessor, this.pullMessageExecutor); + + /** + * pollingInfoProcessor + */ + remotingServer.registerProcessor(RequestCode.POLLING_INFO, this.pollingInfoProcessor, this.pullMessageExecutor); + + /** + * ReplyMessageProcessor + */ + + replyMessageProcessor.registerSendMessageHook(sendMessageHookList); + + remotingServer.registerProcessor(RequestCode.SEND_REPLY_MESSAGE, replyMessageProcessor, replyMessageExecutor); + remotingServer.registerProcessor(RequestCode.SEND_REPLY_MESSAGE_V2, replyMessageProcessor, replyMessageExecutor); + fastRemotingServer.registerProcessor(RequestCode.SEND_REPLY_MESSAGE, replyMessageProcessor, replyMessageExecutor); + fastRemotingServer.registerProcessor(RequestCode.SEND_REPLY_MESSAGE_V2, replyMessageProcessor, replyMessageExecutor); + + /** + * QueryMessageProcessor + */ + NettyRequestProcessor queryProcessor = new QueryMessageProcessor(this); + remotingServer.registerProcessor(RequestCode.QUERY_MESSAGE, queryProcessor, this.queryMessageExecutor); + remotingServer.registerProcessor(RequestCode.VIEW_MESSAGE_BY_ID, queryProcessor, this.queryMessageExecutor); + + fastRemotingServer.registerProcessor(RequestCode.QUERY_MESSAGE, queryProcessor, this.queryMessageExecutor); + fastRemotingServer.registerProcessor(RequestCode.VIEW_MESSAGE_BY_ID, queryProcessor, this.queryMessageExecutor); + + /** + * ClientManageProcessor + */ + remotingServer.registerProcessor(RequestCode.HEART_BEAT, clientManageProcessor, this.heartbeatExecutor); + remotingServer.registerProcessor(RequestCode.UNREGISTER_CLIENT, clientManageProcessor, this.clientManageExecutor); + remotingServer.registerProcessor(RequestCode.CHECK_CLIENT_CONFIG, clientManageProcessor, this.clientManageExecutor); + + fastRemotingServer.registerProcessor(RequestCode.HEART_BEAT, clientManageProcessor, this.heartbeatExecutor); + fastRemotingServer.registerProcessor(RequestCode.UNREGISTER_CLIENT, clientManageProcessor, this.clientManageExecutor); + fastRemotingServer.registerProcessor(RequestCode.CHECK_CLIENT_CONFIG, clientManageProcessor, this.clientManageExecutor); + + /** + * ConsumerManageProcessor + */ + ConsumerManageProcessor consumerManageProcessor = new ConsumerManageProcessor(this); + remotingServer.registerProcessor(RequestCode.GET_CONSUMER_LIST_BY_GROUP, consumerManageProcessor, this.consumerManageExecutor); + remotingServer.registerProcessor(RequestCode.UPDATE_CONSUMER_OFFSET, consumerManageProcessor, this.consumerManageExecutor); + remotingServer.registerProcessor(RequestCode.QUERY_CONSUMER_OFFSET, consumerManageProcessor, this.consumerManageExecutor); + + fastRemotingServer.registerProcessor(RequestCode.GET_CONSUMER_LIST_BY_GROUP, consumerManageProcessor, this.consumerManageExecutor); + fastRemotingServer.registerProcessor(RequestCode.UPDATE_CONSUMER_OFFSET, consumerManageProcessor, this.consumerManageExecutor); + fastRemotingServer.registerProcessor(RequestCode.QUERY_CONSUMER_OFFSET, consumerManageProcessor, this.consumerManageExecutor); + + /** + * QueryAssignmentProcessor + */ + remotingServer.registerProcessor(RequestCode.QUERY_ASSIGNMENT, queryAssignmentProcessor, loadBalanceExecutor); + fastRemotingServer.registerProcessor(RequestCode.QUERY_ASSIGNMENT, queryAssignmentProcessor, loadBalanceExecutor); + remotingServer.registerProcessor(RequestCode.SET_MESSAGE_REQUEST_MODE, queryAssignmentProcessor, loadBalanceExecutor); + fastRemotingServer.registerProcessor(RequestCode.SET_MESSAGE_REQUEST_MODE, queryAssignmentProcessor, loadBalanceExecutor); + + /** + * EndTransactionProcessor + */ + remotingServer.registerProcessor(RequestCode.END_TRANSACTION, endTransactionProcessor, this.endTransactionExecutor); + fastRemotingServer.registerProcessor(RequestCode.END_TRANSACTION, endTransactionProcessor, this.endTransactionExecutor); + + /* + * Default + */ + AdminBrokerProcessor adminProcessor = new AdminBrokerProcessor(this); + remotingServer.registerDefaultProcessor(adminProcessor, this.adminBrokerExecutor); + fastRemotingServer.registerDefaultProcessor(adminProcessor, this.adminBrokerExecutor); + + /* + * Initialize the mapping of request codes to request headers. + */ + RequestHeaderRegistry.getInstance().initialize(); + } + + 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 long headSlowTimeMills4AckThreadPoolQueue() { + return this.headSlowTimeMills(this.ackThreadPoolQueue); + } + + public long headSlowTimeMills4EndTransactionThreadPoolQueue() { + return this.headSlowTimeMills(this.endTransactionThreadPoolQueue); + } + + public long headSlowTimeMills4ClientManagerThreadPoolQueue() { + return this.headSlowTimeMills(this.clientManagerThreadPoolQueue); + } + + public long headSlowTimeMills4HeartbeatThreadPoolQueue() { + return this.headSlowTimeMills(this.heartbeatThreadPoolQueue); + } + + public long headSlowTimeMills4AdminBrokerThreadPoolQueue() { + return this.headSlowTimeMills(this.adminBrokerThreadPoolQueue); + } + + public void printWaterMark() { + logWaterMarkQueueInfo("Send", this.sendThreadPoolQueue, this::headSlowTimeMills4SendThreadPoolQueue); + logWaterMarkQueueInfo("Pull", this.pullThreadPoolQueue, this::headSlowTimeMills4PullThreadPoolQueue); + logWaterMarkQueueInfo("Query", this.queryThreadPoolQueue, this::headSlowTimeMills4QueryThreadPoolQueue); + logWaterMarkQueueInfo("Lite Pull", this.litePullThreadPoolQueue, this::headSlowTimeMills4LitePullThreadPoolQueue); + logWaterMarkQueueInfo("Transaction", this.endTransactionThreadPoolQueue, this::headSlowTimeMills4EndTransactionThreadPoolQueue); + logWaterMarkQueueInfo("ClientManager", this.clientManagerThreadPoolQueue, this::headSlowTimeMills4ClientManagerThreadPoolQueue); + logWaterMarkQueueInfo("Heartbeat", this.heartbeatThreadPoolQueue, this::headSlowTimeMills4HeartbeatThreadPoolQueue); + logWaterMarkQueueInfo("Ack", this.ackThreadPoolQueue, this::headSlowTimeMills4AckThreadPoolQueue); + logWaterMarkQueueInfo("Admin", this.adminBrokerThreadPoolQueue, this::headSlowTimeMills4AdminBrokerThreadPoolQueue); + } + + private void logWaterMarkQueueInfo(String queueName, BlockingQueue queue, Supplier slowTimeSupplier) { + LOG_WATER_MARK.info("[WATERMARK] {} Queue Size: {} SlowTimeMills: {}", queueName, queue.size(), slowTimeSupplier.get()); + } + + 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() { + return broker2Client; + } + + public ConsumerManager getConsumerManager() { + return consumerManager; + } + + public ConsumerFilterManager getConsumerFilterManager() { + return consumerFilterManager; + } + + public ConsumerOrderInfoManager getConsumerOrderInfoManager() { + return consumerOrderInfoManager; + } + + public PopInflightMessageCounter getPopInflightMessageCounter() { + return popInflightMessageCounter; + } + + public PopConsumerService getPopConsumerService() { + return popConsumerService; + } + + public ConsumerOffsetManager getConsumerOffsetManager() { + return consumerOffsetManager; + } + + public void setConsumerOffsetManager(ConsumerOffsetManager consumerOffsetManager) { + this.consumerOffsetManager = consumerOffsetManager; + } + + + public BroadcastOffsetManager getBroadcastOffsetManager() { + return broadcastOffsetManager; + } + + public MessageStoreConfig getMessageStoreConfig() { + return messageStoreConfig; + } + + public ProducerManager getProducerManager() { + return producerManager; + } + + public PullMessageProcessor getPullMessageProcessor() { + return pullMessageProcessor; + } + + 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); + } + + for (Map.Entry entry : remotingServerMap.entrySet()) { + RemotingServer remotingServer = entry.getValue(); + if (remotingServer != null) { + remotingServer.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(); + } + + if (this.popConsumerService != null) { + this.popConsumerService.shutdown(); + } + + if (this.popMessageProcessor.getPopLongPollingService() != null) { + this.popMessageProcessor.getPopLongPollingService().shutdown(); + } + + if (this.popMessageProcessor.getQueueLockManager() != null) { + this.popMessageProcessor.getQueueLockManager().shutdown(); + } + + if (this.popMessageProcessor.getPopBufferMergeService() != null) { + this.popMessageProcessor.getPopBufferMergeService().shutdown(); + } + + if (this.ackMessageProcessor.getPopReviveServices() != null) { + 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.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) { + this.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(); + } + + if (this.consumerOrderInfoManager != null) { + this.consumerOrderInfoManager.persist(); + this.consumerOrderInfoManager.shutdown(); + } + + if (this.configStorage != null) { + this.configStorage.shutdown(); + } + + if (this.authenticationMetadataManager != null) { + this.authenticationMetadataManager.shutdown(); + } + + if (this.authorizationMetadataManager != null) { + this.authorizationMetadataManager.shutdown(); + } + + 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(); + } + + for (Map.Entry entry : remotingServerMap.entrySet()) { + RemotingServer remotingServer = entry.getValue(); + if (remotingServer != null) { + remotingServer.start(); + + if (TCP_REMOTING_SERVER.equals(entry.getKey())) { + // 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()); + } + } + } + } + + 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(); + if (brokerConfig.isPopConsumerFSServiceInit()) { + this.popMessageProcessor.getPopBufferMergeService().start(); + } + this.popMessageProcessor.getQueueLockManager().start(); + } + + if (this.ackMessageProcessor != null) { + if (brokerConfig.isPopConsumerFSServiceInit()) { + this.ackMessageProcessor.startPopReviveService(); + } + } + + if (this.notificationProcessor != null) { + this.notificationProcessor.getPopLongPollingService().start(); + } + + if (this.popConsumerService != null) { + this.popConsumerService.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 synchronized void registerIncrementBrokerData(List topicConfigList, DataVersion dataVersion) { + if (topicConfigList == null || topicConfigList.isEmpty()) { + return; + } + + 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); + } + + 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(); + } + } + + 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); + } + } + + 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()); + + handleRegisterBrokerResult(registerBrokerResultList, checkOrderConfig); + } + + protected void sendHeartbeat() { + if (this.brokerConfig.isEnableControllerMode()) { + this.replicasManager.sendHeartbeatToController(); + } + + 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()); + } + } + } + + protected void syncBrokerMemberGroup() { + try { + 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; + } + 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.isIsolated) { + long minBrokerId = brokerMemberGroup.minimumBrokerId(); + this.updateMinBroker(minBrokerId, brokerMemberGroup.getBrokerAddrs().get(minBrokerId)); + } + } + + private int calcAliveBrokerNumInGroup(Map brokerAddrTable) { + if (brokerAddrTable.containsKey(this.brokerConfig.getBrokerId())) { + return brokerAddrTable.size(); + } else { + return brokerAddrTable.size() + 1; + } + } + + 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; + } + } + } + + private boolean needRegister(final String clusterName, + final String brokerAddr, + final String brokerName, + final long brokerId, + final int timeoutMills, + final boolean isInBrokerContainer) { + + 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; + } + + 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; + + this.changeSpecialServiceStatus(this.brokerConfig.getBrokerId() == minBrokerId); + this.registerBrokerAll(true, false, brokerConfig.isForceRegister()); + + isIsolated = false; + } + + 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 void stopService() { + BrokerController.LOG.info("{} stop service", this.getBrokerConfig().getCanonicalName()); + isIsolated = true; + this.changeSpecialServiceStatus(false); + } + + public boolean isSpecialServiceRunning() { + if (isScheduleServiceStart() && isTransactionCheckServiceStart()) { + return true; + } + + return this.ackMessageProcessor != null && this.ackMessageProcessor.isPopReviveServiceRunning(); + } + + 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); + } + + 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 (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); + } + } + + // set master HA address. + if (masterHaAddr != null) { + this.messageStore.updateHaMasterAddress(masterHaAddr); + } + + // 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(); + } + + if (minBrokerId == MixAll.MASTER_ID && minBrokerAddr != null) { + // master online + onMasterOnline(minBrokerAddr, masterHaAddr); + } + + // notify PullRequest on hold to pull from master. + if (this.minBrokerIdInGroup == MixAll.MASTER_ID) { + this.pullRequestHoldService.notifyMasterOnline(); + } + } + + public void updateMinBroker(long minBrokerId, String minBrokerAddr) { + if (brokerConfig.isEnableSlaveActingMaster() && brokerConfig.getBrokerId() != MixAll.MASTER_ID) { + if (lock.tryLock()) { + try { + if (minBrokerId != this.minBrokerIdInGroup) { + String offlineBrokerAddr = null; + if (minBrokerId > this.minBrokerIdInGroup) { + offlineBrokerAddr = this.minBrokerAddrInGroup; + } + onMinBrokerChange(minBrokerId, minBrokerAddr, offlineBrokerAddr, null); + } + } finally { + lock.unlock(); + } + } + } + } + + 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 void changeSpecialServiceStatus(boolean shouldStart) { + + for (BrokerAttachedPlugin brokerAttachedPlugin : brokerAttachedPlugins) { + if (brokerAttachedPlugin != null) { + brokerAttachedPlugin.statusChanged(shouldStart); + } + } + + changeScheduleServiceStatus(shouldStart); + + 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; + } + } + + 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 (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; + } + + public void setTopicConfigManager(TopicConfigManager topicConfigManager) { + this.topicConfigManager = topicConfigManager; + } + + public TopicQueueMappingManager getTopicQueueMappingManager() { + return topicQueueMappingManager; + } + + public AuthenticationMetadataManager getAuthenticationMetadataManager() { + return authenticationMetadataManager; + } + + @VisibleForTesting + public void setAuthenticationMetadataManager( + AuthenticationMetadataManager authenticationMetadataManager) { + this.authenticationMetadataManager = authenticationMetadataManager; + } + + public AuthorizationMetadataManager getAuthorizationMetadataManager() { + return authorizationMetadataManager; + } + + @VisibleForTesting + public void setAuthorizationMetadataManager( + AuthorizationMetadataManager authorizationMetadataManager) { + this.authorizationMetadataManager = authorizationMetadataManager; + } + + public String getHAServerAddr() { + return this.brokerConfig.getBrokerIP2() + ":" + this.messageStoreConfig.getHaListenPort(); + } + + public RebalanceLockManager getRebalanceLockManager() { + return rebalanceLockManager; + } + + 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; + } + + public BlockingQueue getSendThreadPoolQueue() { + return sendThreadPoolQueue; + } + + public BlockingQueue getAckThreadPoolQueue() { + return ackThreadPoolQueue; + } + + public BrokerStatsManager getBrokerStatsManager() { + return brokerStatsManager; + } + + public void setBrokerStatsManager(BrokerStatsManager brokerStatsManager) { + this.brokerStatsManager = brokerStatsManager; + } + + public List getSendMessageHookList() { + return sendMessageHookList; + } + + public void registerSendMessageHook(final SendMessageHook hook) { + this.sendMessageHookList.add(hook); + LOG.info("register SendMessageHook Hook, {}", hook.hookName()); + } + + public List getConsumeMessageHookList() { + return consumeMessageHookList; + } + + public void registerConsumeMessageHook(final ConsumeMessageHook hook) { + this.consumeMessageHookList.add(hook); + LOG.info("register ConsumeMessageHook Hook, {}", hook.hookName()); + } + + public void registerServerRPCHook(RPCHook rpcHook) { + for (Map.Entry entry : remotingServerMap.entrySet()) { + RemotingServer remotingServer = entry.getValue(); + if (remotingServer != null) { + remotingServer.registerRPCHook(rpcHook); + } + } + } + + public void setRequestPipeline(RequestPipeline pipeline) { + for (Map.Entry entry : remotingServerMap.entrySet()) { + RemotingServer remotingServer = entry.getValue(); + if (remotingServer != null) { + remotingServer.setRequestPipeline(pipeline); + } + } + } + + public RemotingServer getRemotingServer() { + return remotingServerMap.get(TCP_REMOTING_SERVER); + } + + public void setRemotingServer(RemotingServer remotingServer) { + remotingServerMap.put(TCP_REMOTING_SERVER, remotingServer); + } + + public RemotingServer getFastRemotingServer() { + return remotingServerMap.get(FAST_REMOTING_SERVER); + } + + public void setFastRemotingServer(RemotingServer fastRemotingServer) { + remotingServerMap.put(FAST_REMOTING_SERVER, fastRemotingServer); + } + + public RemotingServer getRemotingServerByName(String name) { + return remotingServerMap.get(name); + } + + public void setRemotingServerByName(String name, RemotingServer remotingServer) { + remotingServerMap.put(name, remotingServer); + } + + public ClientHousekeepingService getClientHousekeepingService() { + return clientHousekeepingService; + } + + public CountDownLatch getRemotingServerStartLatch() { + return remotingServerStartLatch; + } + + public void setRemotingServerStartLatch(CountDownLatch remotingServerStartLatch) { + this.remotingServerStartLatch = remotingServerStartLatch; + } + + public void registerClientRPCHook(RPCHook rpcHook) { + this.getBrokerOuterAPI().registerRPCHook(rpcHook); + } + + public BrokerOuterAPI getBrokerOuterAPI() { + return brokerOuterAPI; + } + + public InetSocketAddress getStoreHost() { + return storeHost; + } + + public void setStoreHost(InetSocketAddress storeHost) { + this.storeHost = 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 ExecutorService getSendMessageExecutor() { + return sendMessageExecutor; + } + + public SendMessageProcessor getSendMessageProcessor() { + return sendMessageProcessor; + } + + public RecallMessageProcessor getRecallMessageProcessor() { + return recallMessageProcessor; + } + + 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 new file mode 100644 index 0000000..1c37774 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/BrokerPathConfigHelper.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.broker; + +import java.io.File; + +public class BrokerPathConfigHelper { + private static String brokerConfigPath = System.getProperty("user.home") + File.separator + "store" + + File.separator + "config" + File.separator + "broker.properties"; + + public static String getBrokerConfigPath() { + return brokerConfigPath; + } + + public static void setBrokerConfigPath(String path) { + brokerConfigPath = path; + } + + public static String getTopicConfigPath(final String rootDir) { + return getConfigDir(rootDir) + "topics.json"; + } + + public static String getTopicQueueMappingPath(final String rootDir) { + return getConfigDir(rootDir) + "topicQueueMapping.json"; + } + + public static String getConsumerOffsetPath(final String rootDir) { + return getConfigDir(rootDir) + "consumerOffset.json"; + } + + public static String getLmqConsumerOffsetPath(final String rootDir) { + return getConfigDir(rootDir) + "lmqConsumerOffset.json"; + } + + public static String getConsumerOrderInfoPath(final String rootDir) { + return getConfigDir(rootDir) + "consumerOrderInfo.json"; + } + + public static String getSubscriptionGroupPath(final String rootDir) { + return getConfigDir(rootDir) + "subscriptionGroup.json"; + } + public static String getTimerCheckPath(final String rootDir) { + return getConfigDir(rootDir) + "timercheck"; + } + public static String getTimerMetricsPath(final String rootDir) { + return getConfigDir(rootDir) + "timermetrics"; + } + public static String getTransactionMetricsPath(final String rootDir) { + return getConfigDir(rootDir) + "transactionMetrics"; + } + + public static String getConsumerFilterPath(final String rootDir) { + return getConfigDir(rootDir) + "consumerFilter.json"; + } + + public static String getMessageRequestModePath(final String rootDir) { + return getConfigDir(rootDir) + "messageRequestMode.json"; + } + + private static String getConfigDir(final String rootDir) { + return rootDir + File.separator + "config" + File.separator; + } +} 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 0000000..de2ccb2 --- /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 new file mode 100644 index 0000000..90006b0 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/BrokerStartup.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.broker; + +import java.io.BufferedInputStream; +import java.io.File; +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.lang3.StringUtils; +import org.apache.rocketmq.auth.config.AuthConfig; +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.protocol.RemotingCommand; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.config.MessageStoreConfig; + +public class BrokerStartup { + + public static Logger log; + public static final SystemConfigFileHelper CONFIG_FILE_HELPER = new SystemConfigFileHelper(); + + public static void main(String[] args) { + start(createBrokerController(args)); + } + + public static BrokerController start(BrokerController controller) { + try { + controller.start(); + + 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(); + System.exit(-1); + } + + return null; + } + + 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)); + + final BrokerConfig brokerConfig = new BrokerConfig(); + final NettyServerConfig nettyServerConfig = new NettyServerConfig(); + final NettyClientConfig nettyClientConfig = new NettyClientConfig(); + final MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + final AuthConfig authConfig = new AuthConfig(); + 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); + } + + 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(); + } + } + + if (properties != null) { + properties2SystemEnv(properties); + MixAll.properties2Object(properties, brokerConfig); + MixAll.properties2Object(properties, nettyServerConfig); + MixAll.properties2Object(properties, nettyClientConfig); + MixAll.properties2Object(properties, messageStoreConfig); + MixAll.properties2Object(properties, authConfig); + } + + 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); + } + + // 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); + } + } + + 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() <= 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); + } + + brokerConfig.setInBrokerContainer(false); + + 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); + + authConfig.setConfigName(brokerConfig.getBrokerName()); + authConfig.setClusterName(brokerConfig.getBrokerClusterName()); + authConfig.setAuthConfigPath(messageStoreConfig.getStorePathRootDir() + File.separator + "config"); + + final BrokerController controller = new BrokerController( + brokerConfig, nettyServerConfig, nettyClientConfig, messageStoreConfig, authConfig); + + // 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(buildShutdownHook(controller))); + return controller; + } 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("c", "configFile", true, "Broker 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); + + opt = new Option("m", "printImportantConfig", false, "Print important config item"); + 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(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/RocksDBConfigManager.java b/broker/src/main/java/org/apache/rocketmq/broker/RocksDBConfigManager.java new file mode 100644 index 0000000..ee2d4e5 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/RocksDBConfigManager.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alibaba.fastjson.JSON; +import java.nio.charset.StandardCharsets; +import java.util.function.BiConsumer; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.config.ConfigRocksDBStorage; +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.rocksdb.CompressionType; +import org.rocksdb.FlushOptions; +import org.rocksdb.RocksIterator; +import org.rocksdb.Statistics; +import org.rocksdb.WriteBatch; + +public class RocksDBConfigManager { + protected static final Logger BROKER_LOG = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + public volatile boolean isStop = false; + public ConfigRocksDBStorage configRocksDBStorage = null; + private FlushOptions flushOptions = null; + private volatile long lastFlushMemTableMicroSecond = 0; + private final String filePath; + private final long memTableFlushInterval; + private final CompressionType compressionType; + private DataVersion kvDataVersion = new DataVersion(); + + public RocksDBConfigManager(String filePath, long memTableFlushInterval, CompressionType compressionType) { + this.filePath = filePath; + this.memTableFlushInterval = memTableFlushInterval; + this.compressionType = compressionType; + } + + public boolean init() { + this.isStop = false; + this.configRocksDBStorage = new ConfigRocksDBStorage(filePath, compressionType); + return this.configRocksDBStorage.start(); + } + public boolean loadDataVersion() { + String currDataVersionString = null; + try { + byte[] dataVersion = this.configRocksDBStorage.getKvDataVersion(); + if (dataVersion != null && dataVersion.length > 0) { + currDataVersionString = new String(dataVersion, StandardCharsets.UTF_8); + } + kvDataVersion = StringUtils.isNotBlank(currDataVersionString) ? JSON.parseObject(currDataVersionString, DataVersion.class) : new DataVersion(); + return true; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public boolean loadData(BiConsumer biConsumer) { + try (RocksIterator iterator = this.configRocksDBStorage.iterator()) { + iterator.seekToFirst(); + while (iterator.isValid()) { + biConsumer.accept(iterator.key(), iterator.value()); + iterator.next(); + } + } + + 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 updateKvDataVersion() throws Exception { + kvDataVersion.nextVersion(); + this.configRocksDBStorage.updateKvDataVersion(JSON.toJSONString(kvDataVersion).getBytes(StandardCharsets.UTF_8)); + } + + public DataVersion getKvDataVersion() { + return kvDataVersion; + } + + public void updateForbidden(String key, String value) throws Exception { + this.configRocksDBStorage.updateForbidden(key.getBytes(StandardCharsets.UTF_8), value.getBytes(StandardCharsets.UTF_8)); + } + + + 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/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 0000000..63567f8 --- /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/auth/converter/AclConverter.java b/broker/src/main/java/org/apache/rocketmq/broker/auth/converter/AclConverter.java new file mode 100644 index 0000000..8cef5c6 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/auth/converter/AclConverter.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.broker.auth.converter; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.commons.collections.CollectionUtils; +import org.apache.rocketmq.auth.authentication.model.Subject; +import org.apache.rocketmq.auth.authorization.enums.Decision; +import org.apache.rocketmq.auth.authorization.enums.PolicyType; +import org.apache.rocketmq.auth.authorization.model.Acl; +import org.apache.rocketmq.auth.authorization.model.Environment; +import org.apache.rocketmq.auth.authorization.model.Policy; +import org.apache.rocketmq.auth.authorization.model.PolicyEntry; +import org.apache.rocketmq.auth.authorization.model.Resource; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.remoting.protocol.body.AclInfo; + +public class AclConverter { + + public static Acl convertAcl(AclInfo aclInfo) { + if (aclInfo == null) { + return null; + } + Subject subject = Subject.of(aclInfo.getSubject()); + List policies = new ArrayList<>(); + for (AclInfo.PolicyInfo policy : aclInfo.getPolicies()) { + PolicyType policyType = PolicyType.getByName(policy.getPolicyType()); + + List entryInfos = policy.getEntries(); + if (CollectionUtils.isEmpty(entryInfos)) { + continue; + } + List entries = new ArrayList<>(); + for (AclInfo.PolicyEntryInfo entryInfo : entryInfos) { + Resource resource = Resource.of(entryInfo.getResource()); + + List actions = new ArrayList<>(); + for (String a : entryInfo.getActions()) { + Action action = Action.getByName(a); + if (action == null) { + continue; + } + actions.add(action); + } + + Environment environment = new Environment(); + if (CollectionUtils.isNotEmpty(entryInfo.getSourceIps())) { + environment.setSourceIps(entryInfo.getSourceIps()); + } + + Decision decision = Decision.getByName(entryInfo.getDecision()); + + entries.add(PolicyEntry.of(resource, actions, environment, decision)); + } + + policies.add(Policy.of(policyType, entries)); + } + + return Acl.of(subject, policies); + } + + public static List convertAcls(List acls) { + if (CollectionUtils.isEmpty(acls)) { + return null; + } + return acls.stream().map(AclConverter::convertAcl) + .collect(Collectors.toList()); + } + + public static AclInfo convertAcl(Acl acl) { + if (acl == null) { + return null; + } + AclInfo aclInfo = new AclInfo(); + aclInfo.setSubject(acl.getSubject().getSubjectKey()); + if (CollectionUtils.isEmpty(acl.getPolicies())) { + return aclInfo; + } + List policyInfos = acl.getPolicies().stream() + .map(AclConverter::convertPolicy) + .collect(Collectors.toList()); + aclInfo.setPolicies(policyInfos); + return aclInfo; + } + + private static AclInfo.PolicyInfo convertPolicy(Policy policy) { + AclInfo.PolicyInfo policyInfo = new AclInfo.PolicyInfo(); + if (policy.getPolicyType() != null) { + policyInfo.setPolicyType(policy.getPolicyType().getName()); + } + if (CollectionUtils.isEmpty(policy.getEntries())) { + return policyInfo; + } + List entryInfos = policy.getEntries().stream() + .map(AclConverter::convertPolicyEntry).collect(Collectors.toList()); + policyInfo.setEntries(entryInfos); + return policyInfo; + } + + private static AclInfo.PolicyEntryInfo convertPolicyEntry(PolicyEntry entry) { + AclInfo.PolicyEntryInfo entryInfo = new AclInfo.PolicyEntryInfo(); + entryInfo.setResource(entry.toResourceStr()); + entryInfo.setActions(entry.toActionsStr()); + if (entry.getEnvironment() != null) { + entryInfo.setSourceIps(entry.getEnvironment().getSourceIps()); + } + entryInfo.setDecision(entry.getDecision().getName()); + return entryInfo; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/auth/converter/UserConverter.java b/broker/src/main/java/org/apache/rocketmq/broker/auth/converter/UserConverter.java new file mode 100644 index 0000000..12756d8 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/auth/converter/UserConverter.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.broker.auth.converter; + +import java.util.List; +import java.util.stream.Collectors; +import org.apache.rocketmq.auth.authentication.enums.UserStatus; +import org.apache.rocketmq.auth.authentication.enums.UserType; +import org.apache.rocketmq.auth.authentication.model.User; +import org.apache.rocketmq.remoting.protocol.body.UserInfo; + +public class UserConverter { + + public static List convertUsers(List users) { + return users.stream().map(UserConverter::convertUser) + .collect(Collectors.toList()); + } + + public static UserInfo convertUser(User user) { + UserInfo result = new UserInfo(); + result.setUsername(user.getUsername()); + result.setPassword(user.getPassword()); + if (user.getUserType() != null) { + result.setUserType(user.getUserType().getName()); + } + if (user.getUserStatus() != null) { + result.setUserStatus(user.getUserStatus().getName()); + } + return result; + } + + public static User convertUser(UserInfo userInfo) { + User result = new User(); + result.setUsername(userInfo.getUsername()); + result.setPassword(userInfo.getPassword()); + result.setUserType(UserType.getByName(userInfo.getUserType())); + result.setUserStatus(UserStatus.getByName(userInfo.getUserStatus())); + return result; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/auth/pipeline/AuthenticationPipeline.java b/broker/src/main/java/org/apache/rocketmq/broker/auth/pipeline/AuthenticationPipeline.java new file mode 100644 index 0000000..c38e415 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/auth/pipeline/AuthenticationPipeline.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.auth.pipeline; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.auth.authentication.AuthenticationEvaluator; +import org.apache.rocketmq.auth.authentication.context.AuthenticationContext; +import org.apache.rocketmq.auth.authentication.exception.AuthenticationException; +import org.apache.rocketmq.auth.authentication.factory.AuthenticationFactory; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.common.AbortProcessException; +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.pipeline.RequestPipeline; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.ResponseCode; + +public class AuthenticationPipeline implements RequestPipeline { + protected static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + private final AuthConfig authConfig; + private final AuthenticationEvaluator evaluator; + + public AuthenticationPipeline(AuthConfig authConfig) { + this.authConfig = authConfig; + this.evaluator = AuthenticationFactory.getEvaluator(authConfig); + } + + @Override + public void execute(ChannelHandlerContext ctx, RemotingCommand request) throws Exception { + if (!authConfig.isAuthenticationEnabled()) { + return; + } + try { + AuthenticationContext authenticationContext = newContext(ctx, request); + evaluator.evaluate(authenticationContext); + } catch (AuthenticationException ex) { + throw new AbortProcessException(ResponseCode.NO_PERMISSION, ex.getMessage()); + } catch (Throwable ex) { + LOGGER.error("authenticate failed, request:{}", request, ex); + throw ex; + } + } + + protected AuthenticationContext newContext(ChannelHandlerContext ctx, RemotingCommand request) { + return AuthenticationFactory.newContext(authConfig, ctx, request); + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/auth/pipeline/AuthorizationPipeline.java b/broker/src/main/java/org/apache/rocketmq/broker/auth/pipeline/AuthorizationPipeline.java new file mode 100644 index 0000000..c588dae --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/auth/pipeline/AuthorizationPipeline.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.auth.pipeline; + +import io.netty.channel.ChannelHandlerContext; +import java.util.List; +import org.apache.rocketmq.auth.authentication.exception.AuthenticationException; +import org.apache.rocketmq.auth.authorization.AuthorizationEvaluator; +import org.apache.rocketmq.auth.authorization.context.AuthorizationContext; +import org.apache.rocketmq.auth.authorization.exception.AuthorizationException; +import org.apache.rocketmq.auth.authorization.factory.AuthorizationFactory; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.common.AbortProcessException; +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.pipeline.RequestPipeline; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.ResponseCode; + +public class AuthorizationPipeline implements RequestPipeline { + protected static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + private final AuthConfig authConfig; + private final AuthorizationEvaluator evaluator; + + public AuthorizationPipeline(AuthConfig authConfig) { + this.authConfig = authConfig; + this.evaluator = AuthorizationFactory.getEvaluator(authConfig); + } + + @Override + public void execute(ChannelHandlerContext ctx, RemotingCommand request) throws Exception { + if (!authConfig.isAuthorizationEnabled()) { + return; + } + try { + List contexts = newContexts(ctx, request); + evaluator.evaluate(contexts); + } catch (AuthorizationException | AuthenticationException ex) { + throw new AbortProcessException(ResponseCode.NO_PERMISSION, ex.getMessage()); + } catch (Throwable ex) { + LOGGER.error("authorization failed, request:{}", request, ex); + throw ex; + } + } + + protected List newContexts(ChannelHandlerContext ctx, RemotingCommand request) { + return AuthorizationFactory.newContexts(authConfig, ctx, request); + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/client/ClientChannelAttributeHelper.java b/broker/src/main/java/org/apache/rocketmq/broker/client/ClientChannelAttributeHelper.java new file mode 100644 index 0000000..2908539 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/client/ClientChannelAttributeHelper.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.broker.client; + +import io.netty.channel.Channel; +import io.netty.util.AttributeKey; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class ClientChannelAttributeHelper { + private static final AttributeKey ATTR_CG = AttributeKey.valueOf("CHANNEL_CONSUMER_GROUP"); + private static final AttributeKey ATTR_PG = AttributeKey.valueOf("CHANNEL_PRODUCER_GROUP"); + private static final String SEPARATOR = "|"; + + public static void addProducerGroup(Channel channel, String group) { + addGroup(channel, group, ATTR_PG); + } + + public static void addConsumerGroup(Channel channel, String group) { + addGroup(channel, group, ATTR_CG); + } + + public static List getProducerGroups(Channel channel) { + return getGroups(channel, ATTR_PG); + } + + public static List getConsumerGroups(Channel channel) { + return getGroups(channel, ATTR_CG); + } + + private static void addGroup(Channel channel, String group, AttributeKey key) { + if (null == channel || !channel.isActive()) { // no side effect if check active status. + return; + } + if (null == group || group.length() == 0 || null == key) { + return; + } + String groups = channel.attr(key).get(); + if (null == groups) { + channel.attr(key).set(group + SEPARATOR); + } else { + if (groups.contains(SEPARATOR + group + SEPARATOR)) { + return; + } else { + channel.attr(key).compareAndSet(groups, groups + group + SEPARATOR); + } + } + } + + private static List getGroups(Channel channel, AttributeKey key) { + if (null == channel) { + return Collections.emptyList(); + } + if (null == key) { + return Collections.emptyList(); + } + String groups = channel.attr(key).get(); + return null == groups ? Collections.emptyList() : Arrays.asList(groups.split("\\|")); + } + +} \ No newline at end of file diff --git a/broker/src/main/java/org/apache/rocketmq/broker/client/ClientChannelInfo.java b/broker/src/main/java/org/apache/rocketmq/broker/client/ClientChannelInfo.java new file mode 100644 index 0000000..edcba96 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/client/ClientChannelInfo.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.broker.client; + +import io.netty.channel.Channel; +import org.apache.rocketmq.remoting.protocol.LanguageCode; + +public class ClientChannelInfo { + private final Channel channel; + private final String clientId; + private final LanguageCode language; + private final int version; + private volatile long lastUpdateTimestamp = System.currentTimeMillis(); + + public ClientChannelInfo(Channel channel) { + this(channel, null, null, 0); + } + + public ClientChannelInfo(Channel channel, String clientId, LanguageCode language, int version) { + this.channel = channel; + this.clientId = clientId; + this.language = language; + this.version = version; + } + + public Channel getChannel() { + return channel; + } + + public String getClientId() { + return clientId; + } + + public LanguageCode getLanguage() { + return language; + } + + public int getVersion() { + return version; + } + + public long getLastUpdateTimestamp() { + return lastUpdateTimestamp; + } + + public void setLastUpdateTimestamp(long lastUpdateTimestamp) { + this.lastUpdateTimestamp = lastUpdateTimestamp; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((channel == null) ? 0 : channel.hashCode()); + result = prime * result + ((clientId == null) ? 0 : clientId.hashCode()); + result = prime * result + ((language == null) ? 0 : language.hashCode()); + result = prime * result + (int) (lastUpdateTimestamp ^ (lastUpdateTimestamp >>> 32)); + result = prime * result + version; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ClientChannelInfo other = (ClientChannelInfo) obj; + if (channel == null) { + if (other.channel != null) + return false; + } else if (this.channel != other.channel) { + return false; + } + + return true; + } + + @Override + public String toString() { + return "ClientChannelInfo [channel=" + channel + ", clientId=" + clientId + ", language=" + language + + ", version=" + version + ", lastUpdateTimestamp=" + lastUpdateTimestamp + "]"; + } +} 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 new file mode 100644 index 0000000..7878d0e --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/client/ClientHousekeepingService.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.client; + +import io.netty.channel.Channel; +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; + +public class ClientHousekeepingService implements ChannelEventListener { + private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + private final BrokerController brokerController; + + private ScheduledExecutorService scheduledExecutorService; + + public ClientHousekeepingService(final BrokerController brokerController) { + this.brokerController = brokerController; + scheduledExecutorService = ThreadUtils.newScheduledThreadPool(1, + new ThreadFactoryImpl("ClientHousekeepingScheduledThread", brokerController.getBrokerIdentity())); + } + + public void start() { + + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + ClientHousekeepingService.this.scanExceptionChannel(); + } catch (Throwable e) { + log.error("Error occurred when scan not active client channels.", e); + } + } + }, 1000 * 10, 1000 * 10, TimeUnit.MILLISECONDS); + } + + private void scanExceptionChannel() { + this.brokerController.getProducerManager().scanNotActiveChannel(); + this.brokerController.getConsumerManager().scanNotActiveChannel(); + } + + public void shutdown() { + this.scheduledExecutorService.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.getBrokerStatsManager().incChannelCloseNum(); + } + + @Override + public void onChannelException(String remoteAddr, Channel channel) { + this.brokerController.getProducerManager().doChannelCloseEvent(remoteAddr, channel); + this.brokerController.getConsumerManager().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.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 new file mode 100644 index 0000000..6c0a58d --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/client/ConsumerGroupEvent.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.client; + +public enum ConsumerGroupEvent { + + /** + * Some consumers in the group are changed. + */ + CHANGE, + /** + * The group of consumer is unregistered. + */ + UNREGISTER, + /** + * The group of consumer is registered. + */ + 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 new file mode 100644 index 0000000..1ea58c1 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/client/ConsumerGroupInfo.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.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 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.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<>(); + private final ConcurrentMap channelInfoTable = + new ConcurrentHashMap<>(16); + private volatile ConsumeType consumeType; + private volatile MessageModel messageModel; + private volatile ConsumeFromWhere consumeFromWhere; + private volatile long lastUpdateTimestamp = System.currentTimeMillis(); + + public ConsumerGroupInfo(String groupName, ConsumeType consumeType, MessageModel messageModel, + ConsumeFromWhere consumeFromWhere) { + this.groupName = groupName; + this.consumeType = consumeType; + this.messageModel = 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()) { + Entry next = it.next(); + if (next.getValue().getClientId().equals(clientId)) { + return next.getValue(); + } + } + + return null; + } + + public ConcurrentMap getSubscriptionTable() { + return subscriptionTable; + } + + public ClientChannelInfo findChannel(final Channel channel) { + return this.channelInfoTable.get(channel); + } + + public ConcurrentMap getChannelInfoTable() { + return channelInfoTable; + } + + public List getAllChannel() { + List result = new ArrayList<>(); + + result.addAll(this.channelInfoTable.keySet()); + + return result; + } + + public List getAllClientId() { + List result = new ArrayList<>(); + + Iterator> it = this.channelInfoTable.entrySet().iterator(); + + while (it.hasNext()) { + Entry entry = it.next(); + ClientChannelInfo clientChannelInfo = entry.getValue(); + result.add(clientChannelInfo.getClientId()); + } + + return result; + } + + 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 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 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; + this.consumeType = consumeType; + this.messageModel = messageModel; + this.consumeFromWhere = consumeFromWhere; + + ClientChannelInfo infoOld = this.channelInfoTable.get(infoNew.getChannel()); + if (null == infoOld) { + ClientChannelInfo prev = this.channelInfoTable.put(infoNew.getChannel(), infoNew); + if (null == prev) { + log.info("new consumer connected, group: {} {} {} channel: {}", this.groupName, consumeType, + messageModel, infoNew.toString()); + updated = true; + } + + infoOld = infoNew; + } else { + if (!infoOld.getClientId().equals(infoNew.getClientId())) { + 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); + } + } + + this.lastUpdateTimestamp = System.currentTimeMillis(); + infoOld.setLastUpdateTimestamp(this.lastUpdateTimestamp); + + 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) { + SubscriptionData prev = this.subscriptionTable.putIfAbsent(sub.getTopic(), sub); + if (null == prev) { + updated = true; + log.info("subscription changed, add new topic, group: {} {}", + this.groupName, + sub.toString()); + } + } else if (sub.getSubVersion() > old.getSubVersion()) { + if (this.consumeType == ConsumeType.CONSUME_PASSIVELY) { + log.info("subscription changed, group: {} OLD: {} NEW: {}", + this.groupName, + old.toString(), + sub.toString() + ); + } + + 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(); + // Check HashSet with O(1) time complexity + if (!topicSet.contains(oldTopic)) { + log.warn("subscription changed, group: {} remove topic {} {}", + this.groupName, + oldTopic, + next.getValue().toString() + ); + + it.remove(); + updated = true; + } + } + + this.lastUpdateTimestamp = System.currentTimeMillis(); + + return updated; + } + + public Set getSubscribeTopics() { + return subscriptionTable.keySet(); + } + + public SubscriptionData findSubscriptionData(final String topic) { + return this.subscriptionTable.get(topic); + } + + 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 String getGroupName() { + return groupName; + } + + public long getLastUpdateTimestamp() { + return lastUpdateTimestamp; + } + + public void setLastUpdateTimestamp(long lastUpdateTimestamp) { + this.lastUpdateTimestamp = lastUpdateTimestamp; + } + + public ConsumeFromWhere getConsumeFromWhere() { + return consumeFromWhere; + } + + public void setConsumeFromWhere(ConsumeFromWhere consumeFromWhere) { + this.consumeFromWhere = consumeFromWhere; + } +} 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 new file mode 100644 index 0000000..144092c --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/client/ConsumerIdsChangeListener.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.broker.client; + +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 new file mode 100644 index 0000000..04238e2 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/client/ConsumerManager.java @@ -0,0 +1,413 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.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.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 LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + private final ConcurrentMap consumerTable = + new ConcurrentHashMap<>(1024); + private final ConcurrentMap> topicGroupTable = + 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; + private final BrokerConfig brokerConfig; + + public ConsumerManager(final ConsumerIdsChangeListener consumerIdsChangeListener, long expiredTimeout) { + this.consumerIdsChangeListenerList.add(consumerIdsChangeListener); + this.brokerStatsManager = null; + this.channelExpiredTimeout = expiredTimeout; + this.subscriptionExpiredTimeout = expiredTimeout; + this.brokerConfig = null; + } + + 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(); + this.brokerConfig = brokerConfig; + } + + public ClientChannelInfo findChannel(final String group, final String clientId) { + ConsumerGroupInfo consumerGroupInfo = this.consumerTable.get(group); + if (consumerGroupInfo != null) { + return consumerGroupInfo.findChannel(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) { + return findSubscriptionData(group, topic, true); + } + + public SubscriptionData findSubscriptionData(final String group, final String topic, + boolean fromCompensationTable) { + ConsumerGroupInfo consumerGroupInfo = getConsumerGroupInfo(group, false); + if (consumerGroupInfo != null) { + 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 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) { + ConsumerGroupInfo consumerGroupInfo = this.getConsumerGroupInfo(group); + if (consumerGroupInfo != null) { + return consumerGroupInfo.getSubscriptionTable().size(); + } + + return 0; + } + + public boolean doChannelCloseEvent(final String remoteAddr, final Channel channel) { + boolean removed = false; + if (this.brokerConfig != null && this.brokerConfig.isEnableFastChannelEventProcess()) { + List groups = ClientChannelAttributeHelper.getConsumerGroups(channel); + if (this.brokerConfig.isPrintChannelGroups() && groups.size() >= 5 && groups.size() >= this.brokerConfig.getPrintChannelGroupsMinNum()) { + LOGGER.warn("channel close event, too many consumer groups one channel, {}, {}, {}", groups.size(), remoteAddr, groups); + } + for (String group : groups) { + if (null == group || group.length() == 0) { + continue; + } + ConsumerGroupInfo consumerGroupInfo = this.consumerTable.get(group); + if (null == consumerGroupInfo) { + continue; + } + ClientChannelInfo clientChannelInfo = consumerGroupInfo.doChannelCloseEvent(remoteAddr, channel); + if (clientChannelInfo != null) { + removed = true; + callConsumerIdsChangeListener(ConsumerGroupEvent.CLIENT_UNREGISTER, group, clientChannelInfo, consumerGroupInfo.getSubscribeTopics()); + if (consumerGroupInfo.getChannelInfoTable().isEmpty()) { + ConsumerGroupInfo remove = this.consumerTable.remove(group); + if (remove != null) { + LOGGER.info("unregister consumer ok, no any connection, and remove consumer group, {}", + group); + callConsumerIdsChangeListener(ConsumerGroupEvent.UNREGISTER, group); + clearTopicGroupTable(remove); + } + } + callConsumerIdsChangeListener(ConsumerGroupEvent.CHANGE, group, consumerGroupInfo.getAllChannel()); + } + } + return removed; + } + Iterator> it = this.consumerTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + ConsumerGroupInfo info = next.getValue(); + ClientChannelInfo clientChannelInfo = info.doChannelCloseEvent(remoteAddr, channel); + if (clientChannelInfo != null) { + removed = true; + callConsumerIdsChangeListener(ConsumerGroupEvent.CLIENT_UNREGISTER, next.getKey(), clientChannelInfo, info.getSubscribeTopics()); + if (info.getChannelInfoTable().isEmpty()) { + ConsumerGroupInfo remove = this.consumerTable.remove(next.getKey()); + if (remove != null) { + LOGGER.info("unregister consumer ok, no any connection, and remove consumer group, {}", + next.getKey()); + callConsumerIdsChangeListener(ConsumerGroupEvent.UNREGISTER, next.getKey()); + clearTopicGroupTable(remove); + } + } + if (!isBroadcastMode(info.getMessageModel())) { + callConsumerIdsChangeListener(ConsumerGroupEvent.CHANGE, next.getKey(), info.getAllChannel()); + } + } + } + return removed; + } + + private void clearTopicGroupTable(final ConsumerGroupInfo groupInfo) { + for (String subscribeTopic : groupInfo.getSubscribeTopics()) { + Set groups = this.topicGroupTable.get(subscribeTopic); + if (groups != null) { + groups.remove(groupInfo.getGroupName()); + } + if (groups != null && groups.isEmpty()) { + this.topicGroupTable.remove(subscribeTopic); + } + } + } + + // 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) { + ConsumerGroupInfo tmp = new ConsumerGroupInfo(group, consumeType, messageModel, consumeFromWhere); + ConsumerGroupInfo prev = this.consumerTable.putIfAbsent(group, tmp); + consumerGroupInfo = prev != null ? prev : tmp; + } + + for (SubscriptionData subscriptionData : subList) { + Set groups = this.topicGroupTable.get(subscriptionData.getTopic()); + if (groups == null) { + Set tmp = new HashSet<>(); + Set prev = this.topicGroupTable.putIfAbsent(subscriptionData.getTopic(), tmp); + groups = prev != null ? prev : tmp; + } + groups.add(subscriptionData.getTopic()); + } + + boolean r1 = + consumerGroupInfo.updateChannel(clientChannelInfo, consumeType, messageModel, + consumeFromWhere); + if (r1) { + callConsumerIdsChangeListener(ConsumerGroupEvent.CLIENT_REGISTER, group, clientChannelInfo, + subList.stream().map(SubscriptionData::getTopic).collect(Collectors.toSet())); + } + boolean r2 = false; + if (updateSubscription) { + r2 = consumerGroupInfo.updateSubscription(subList); + } + + if (r1 || r2) { + if (isNotifyConsumerIdsChangedEnable && !isBroadcastMode(consumerGroupInfo.getMessageModel())) { + callConsumerIdsChangeListener(ConsumerGroupEvent.CHANGE, group, consumerGroupInfo.getAllChannel()); + } + } + + if (this.brokerConfig != null && this.brokerConfig.isEnableFastChannelEventProcess() && r1) { + ClientChannelAttributeHelper.addConsumerGroup(clientChannelInfo.getChannel(), group); + } + + if (null != this.brokerStatsManager) { + this.brokerStatsManager.incConsumerRegisterTime((int) (System.currentTimeMillis() - start)); + } + + 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; + } + + for (SubscriptionData subscriptionData : consumerGroupInfo.getSubscriptionTable().values()) { + Set groups = this.topicGroupTable.get(subscriptionData.getTopic()); + if (groups == null) { + Set tmp = new HashSet<>(); + Set prev = this.topicGroupTable.putIfAbsent(subscriptionData.getTopic(), tmp); + groups = prev != null ? prev : tmp; + } + groups.add(subscriptionData.getTopic()); + } + + boolean updateChannelRst = consumerGroupInfo.updateChannel(clientChannelInfo, consumeType, messageModel, consumeFromWhere); + if (updateChannelRst && isNotifyConsumerIdsChangedEnable && !isBroadcastMode(consumerGroupInfo.getMessageModel())) { + 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) { + 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) { + LOGGER.info("unregister consumer ok, no any connection, and remove consumer group, {}", group); + + callConsumerIdsChangeListener(ConsumerGroupEvent.UNREGISTER, group); + clearTopicGroupTable(remove); + } + } + if (isNotifyConsumerIdsChangedEnable && !isBroadcastMode(consumerGroupInfo.getMessageModel())) { + 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); + } + } + + public void scanNotActiveChannel() { + Iterator> it = this.consumerTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + String group = next.getKey(); + ConsumerGroupInfo consumerGroupInfo = next.getValue(); + ConcurrentMap channelInfoTable = + consumerGroupInfo.getChannelInfoTable(); + + Iterator> itChannel = channelInfoTable.entrySet().iterator(); + while (itChannel.hasNext()) { + Entry nextChannel = itChannel.next(); + ClientChannelInfo clientChannelInfo = nextChannel.getValue(); + long diff = System.currentTimeMillis() - clientChannelInfo.getLastUpdateTimestamp(); + if (diff > channelExpiredTimeout) { + LOGGER.warn( + "SCAN: remove expired channel from ConsumerManager consumerTable. channel={}, consumerGroup={}", + RemotingHelper.parseChannelRemoteAddr(clientChannelInfo.getChannel()), group); + callConsumerIdsChangeListener(ConsumerGroupEvent.CLIENT_UNREGISTER, group, clientChannelInfo, consumerGroupInfo.getSubscribeTopics()); + RemotingHelper.closeChannel(clientChannelInfo.getChannel()); + itChannel.remove(); + } + } + + if (channelInfoTable.isEmpty()) { + LOGGER.warn( + "SCAN: remove expired channel from ConsumerManager consumerTable, all clear, consumerGroup={}", + group); + it.remove(); + } + } + removeExpireConsumerGroupInfo(); + } + + public HashSet queryTopicConsumeByWho(final String topic) { + HashSet groups = new HashSet<>(); + if (this.topicGroupTable.get(topic) != null) { + groups.addAll(this.topicGroupTable.get(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); + } + } + } + + private boolean isBroadcastMode(final MessageModel messageModel) { + return MessageModel.BROADCASTING.equals(messageModel); + } +} 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 new file mode 100644 index 0000000..e046176 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/client/DefaultConsumerIdsChangeListener.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.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 java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.rocketmq.broker.BrokerController; +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); + + private final ConcurrentHashMap activeGroupNotifyMap = new ConcurrentHashMap<>(); + + 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 + public void handle(ConsumerGroupEvent event, String group, Object... args) { + if (event == null) { + return; + } + switch (event) { + case CHANGE: + if (args == null || args.length < 1) { + return; + } + List channels = (List) args[0]; + if (channels != null && brokerController.getBrokerConfig().isNotifyConsumerIdsChangedEnable()) { + if (this.brokerController.getBrokerConfig().isRealTimeNotifyConsumerChange()) { + NotifyTaskControl currentNotifyTaskControl = new NotifyTaskControl(channels); + activeGroupNotifyMap.compute(group, (k, oldVal) -> { + if (null != oldVal) { + oldVal.interrupt(); + } + return currentNotifyTaskControl; + }); + + boolean isNormalCompletion = true; + for (Channel chl : currentNotifyTaskControl.getChannels()) { + if (currentNotifyTaskControl.isInterrupted()) { + isNormalCompletion = false; + break; + } + this.brokerController.getBroker2Client().notifyConsumerIdsChanged(chl, group); + } + if (isNormalCompletion) { + activeGroupNotifyMap.computeIfPresent(group, (k, val) -> val == currentNotifyTaskControl ? null : val); + } + } else { + consumerChannelMap.put(group, channels); + } + } + break; + case UNREGISTER: + this.brokerController.getConsumerFilterManager().unRegister(group); + break; + case REGISTER: + if (args == null || args.length < 1) { + return; + } + 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(); + } + + private static class NotifyTaskControl { + + private final AtomicBoolean interrupted = new AtomicBoolean(false); + + private final List channels; + + public NotifyTaskControl(List channels) { + this.channels = channels; + } + + public boolean isInterrupted() { + return interrupted.get(); + } + + public void interrupt() { + interrupted.set(true); + } + + public List getChannels() { + return channels; + } + } +} 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 0000000..f8183d3 --- /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 0000000..cbf27ce --- /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 new file mode 100644 index 0000000..bc8400c --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/client/ProducerManager.java @@ -0,0 +1,337 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArrayList; +import org.apache.rocketmq.broker.util.PositiveAtomicCounter; +import org.apache.rocketmq.common.BrokerConfig; +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.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 CHANNEL_EXPIRED_TIMEOUT = 1000 * 120; + private static final int GET_AVAILABLE_CHANNEL_RETRY_COUNT = 3; + private final ConcurrentMap> groupChannelTable = + new ConcurrentHashMap<>(); + private final ConcurrentMap clientChannelTable = new ConcurrentHashMap<>(); + protected final BrokerStatsManager brokerStatsManager; + private final BrokerConfig brokerConfig; + private final PositiveAtomicCounter positiveAtomicCounter = new PositiveAtomicCounter(); + private final List producerChangeListenerList = new CopyOnWriteArrayList<>(); + + public ProducerManager() { + this.brokerStatsManager = null; + this.brokerConfig = null; + } + + public ProducerManager(final BrokerStatsManager brokerStatsManager) { + this.brokerStatsManager = brokerStatsManager; + this.brokerConfig = null; + } + + public ProducerManager(final BrokerStatsManager brokerStatsManager, final BrokerConfig brokerConfig) { + this.brokerStatsManager = brokerStatsManager; + this.brokerConfig = brokerConfig; + } + + public int groupSize() { + return this.groupChannelTable.size(); + } + + public boolean groupOnline(String group) { + Map channels = this.groupChannelTable.get(group); + return channels != null && !channels.isEmpty(); + } + + public ConcurrentMap> 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() + )))); + } + } + } + return new ProducerTableInfo(map); + } + + public void scanNotActiveChannel() { + Iterator>> iterator = this.groupChannelTable.entrySet().iterator(); + + while (iterator.hasNext()) { + Map.Entry> entry = iterator.next(); + + final String group = entry.getKey(); + final ConcurrentMap 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()); + } + 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()); + } + } + + 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 boolean doChannelCloseEvent(final String remoteAddr, final Channel channel) { + boolean removed = false; + if (channel != null) { + if (this.brokerConfig != null && this.brokerConfig.isEnableFastChannelEventProcess()) { + List groups = ClientChannelAttributeHelper.getProducerGroups(channel); + if (this.brokerConfig.isPrintChannelGroups() && groups.size() >= 5 && groups.size() >= this.brokerConfig.getPrintChannelGroupsMinNum()) { + log.warn("channel close event, too many producer groups one channel, {}, {}, {}", groups.size(), remoteAddr, groups); + } + for (String group : groups) { + if (null == group || group.length() == 0) { + continue; + } + ConcurrentMap clientChannelInfoTable = this.groupChannelTable.get(group); + if (null == clientChannelInfoTable) { + continue; + } + 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()) { + ConcurrentMap oldGroupTable = this.groupChannelTable.remove(group); + if (oldGroupTable != null) { + log.info("unregister a producer group[{}] from groupChannelTable", group); + callProducerChangeListener(ProducerGroupEvent.GROUP_UNREGISTER, group, null); + } + } + } + } + return removed; // must return here, degrade to scanNotActiveChannel at worst. + } + for (final Map.Entry> entry : this.groupChannelTable.entrySet()) { + final String group = entry.getKey(); + final ConcurrentMap 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()) { + ConcurrentMap oldGroupTable = this.groupChannelTable.remove(group); + if (oldGroupTable != null) { + log.info("unregister a producer group[{}] from groupChannelTable", group); + callProducerChangeListener(ProducerGroupEvent.GROUP_UNREGISTER, group, null); + } + } + } + + } + } + return removed; + } + + public void registerProducer(final String group, final ClientChannelInfo clientChannelInfo) { + + long start = System.currentTimeMillis(); + ClientChannelInfo clientChannelInfoFound; + + ConcurrentMap channelTable = this.groupChannelTable.get(group); + // note that we must take care of the exist groups and channels, + // only can return when groups or channels not exist. + if (this.brokerConfig != null + && !this.brokerConfig.isEnableRegisterProducer() + && this.brokerConfig.isRejectTransactionMessage()) { + boolean needRegister = true; + if (null == channelTable) { + needRegister = false; + } else { + clientChannelInfoFound = channelTable.get(clientChannelInfo.getChannel()); + if (null == clientChannelInfoFound) { + needRegister = false; + } + } + if (!needRegister) { + if (null != this.brokerStatsManager) { + this.brokerStatsManager.incProducerRegisterTime((int) (System.currentTimeMillis() - start)); + } + return; + } + } + + if (null == channelTable) { + channelTable = new ConcurrentHashMap<>(); + ConcurrentMap prev = this.groupChannelTable.putIfAbsent(group, channelTable); + channelTable = prev != null ? prev : channelTable; + } + + clientChannelInfoFound = channelTable.get(clientChannelInfo.getChannel()); + // Add client-channel info to existing producer group + 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 (this.brokerConfig != null && this.brokerConfig.isEnableFastChannelEventProcess()) { + ClientChannelAttributeHelper.addProducerGroup(clientChannelInfo.getChannel(), group); + } + } + + // Refresh existing client-channel-info update-timestamp + if (clientChannelInfoFound != null) { + clientChannelInfoFound.setLastUpdateTimestamp(System.currentTimeMillis()); + } + + if (null != this.brokerStatsManager) { + this.brokerStatsManager.incProducerRegisterTime((int) (System.currentTimeMillis() - start)); + } + } + + public void unregisterProducer(final String group, final ClientChannelInfo clientChannelInfo) { + ConcurrentMap 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); + } + } + } + + public Channel getAvailableChannel(String groupId) { + if (groupId == null) { + return null; + } + List channelList; + ConcurrentMap 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; + } + + 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; + } + 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 new file mode 100644 index 0000000..f898496 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/client/net/Broker2Client.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.client.net; + +import io.netty.channel.Channel; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentMap; +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.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.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.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.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; +import org.apache.rocketmq.store.exception.ConsumeQueueException; + +public class Broker2Client { + private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + private final BrokerController brokerController; + + public Broker2Client(BrokerController brokerController) { + this.brokerController = brokerController; + } + + public void checkProducerTransactionState( + final String group, + final Channel channel, + final CheckTransactionStateRequestHeader requestHeader, + final MessageExt messageExt) throws Exception { + RemotingCommand request = + RemotingCommand.createRequestCommand(RequestCode.CHECK_TRANSACTION_STATE, requestHeader); + request.setBody(MessageDecoder.encode(messageExt, false)); + try { + 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()); + } + } + + public RemotingCommand callClient(final Channel channel, + final RemotingCommand request + ) throws RemotingSendRequestException, RemotingTimeoutException, InterruptedException { + return this.brokerController.getRemotingServer().invokeSync(channel, request, 10000); + } + + public void notifyConsumerIdsChanged( + final Channel channel, + final String consumerGroup) { + if (null == consumerGroup) { + log.error("notifyConsumerIdsChanged consumerGroup is null"); + return; + } + + NotifyConsumerIdsChangedRequestHeader requestHeader = new NotifyConsumerIdsChangedRequestHeader(); + requestHeader.setConsumerGroup(consumerGroup); + RemotingCommand request = + RemotingCommand.createRequestCommand(RequestCode.NOTIFY_CONSUMER_IDS_CHANGED, requestHeader); + + try { + this.brokerController.getRemotingServer().invokeOneway(channel, request, 10); + } catch (Exception e) { + log.error("notifyConsumerIdsChanged exception. group={}, error={}", consumerGroup, e.toString()); + } + } + + public RemotingCommand resetOffset(String topic, String group, long timeStamp, boolean isForce) throws RemotingCommandException { + return resetOffset(topic, group, timeStamp, isForce, false); + } + + public RemotingCommand resetOffset(String topic, String group, long timeStamp, boolean isForce, + boolean isC) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(topic); + if (null == topicConfig) { + log.error("[reset-offset] reset offset failed, no topic in this broker. topic={}", topic); + response.setCode(ResponseCode.TOPIC_NOT_EXIST); + response.setRemark("[reset-offset] reset offset failed, no topic in this broker. topic=" + topic); + return response; + } + + Map offsetTable = new HashMap<>(); + + for (int i = 0; i < topicConfig.getWriteQueueNums(); i++) { + MessageQueue mq = new MessageQueue(); + mq.setBrokerName(this.brokerController.getBrokerConfig().getBrokerName()); + mq.setTopic(topic); + mq.setQueueId(i); + + long consumerOffset = + this.brokerController.getConsumerOffsetManager().queryOffset(group, topic, i); + if (-1 == consumerOffset) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(String.format("THe consumer group <%s> not exist", group)); + return response; + } + + long timeStampOffset; + if (timeStamp == -1) { + try { + timeStampOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, i); + } catch (ConsumeQueueException e) { + throw new RemotingCommandException("Failed to get max offset in queue", e); + } + } else { + timeStampOffset = this.brokerController.getMessageStore().getOffsetInQueueByTime(topic, i, timeStamp); + } + + if (timeStampOffset < 0) { + log.warn("reset offset is invalid. topic={}, queueId={}, timeStampOffset={}", topic, i, timeStampOffset); + timeStampOffset = 0; + } + + if (isForce || timeStampOffset < consumerOffset) { + offsetTable.put(mq, timeStampOffset); + } else { + offsetTable.put(mq, consumerOffset); + } + } + + ResetOffsetRequestHeader requestHeader = new ResetOffsetRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setGroup(group); + requestHeader.setTimestamp(timeStamp); + RemotingCommand request = + RemotingCommand.createRequestCommand(RequestCode.RESET_CONSUMER_CLIENT_OFFSET, requestHeader); + if (isC) { + // c++ language + ResetOffsetBodyForC body = new ResetOffsetBodyForC(); + List offsetList = convertOffsetTable2OffsetList(offsetTable); + body.setOffsetTable(offsetList); + request.setBody(body.encode()); + } else { + // other language + ResetOffsetBody body = new ResetOffsetBody(); + body.setOffsetTable(offsetTable); + request.setBody(body.encode()); + } + + ConsumerGroupInfo consumerGroupInfo = + this.brokerController.getConsumerManager().getConsumerGroupInfo(group); + + if (consumerGroupInfo != null && !consumerGroupInfo.getAllChannel().isEmpty()) { + ConcurrentMap channelInfoTable = + consumerGroupInfo.getChannelInfoTable(); + for (Map.Entry entry : channelInfoTable.entrySet()) { + int version = entry.getValue().getVersion(); + if (version >= MQVersion.Version.V3_0_7_SNAPSHOT.ordinal()) { + try { + this.brokerController.getRemotingServer().invokeOneway(entry.getKey(), request, 5000); + 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={} ,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. channel={}, version={}", + RemotingHelper.parseChannelRemoteAddr(entry.getKey()), MQVersion.getVersionDesc(version)); + return response; + } + } + } else { + String errorInfo = + String.format("Consumer not online, so can not reset offset, Group: %s Topic: %s Timestamp: %d", + requestHeader.getGroup(), + requestHeader.getTopic(), + requestHeader.getTimestamp()); + log.error(errorInfo); + response.setCode(ResponseCode.CONSUMER_NOT_ONLINE); + response.setRemark(errorInfo); + return response; + } + response.setCode(ResponseCode.SUCCESS); + ResetOffsetBody resBody = new ResetOffsetBody(); + resBody.setOffsetTable(offsetTable); + response.setBody(resBody.encode()); + return response; + } + + private List convertOffsetTable2OffsetList(Map table) { + List list = new ArrayList<>(); + for (Entry entry : table.entrySet()) { + MessageQueue mq = entry.getKey(); + MessageQueueForC tmp = + new MessageQueueForC(mq.getTopic(), mq.getBrokerName(), mq.getQueueId(), entry.getValue()); + list.add(tmp); + } + return list; + } + + public RemotingCommand getConsumeStatus(String topic, String group, String originClientId) { + final RemotingCommand result = RemotingCommand.createResponseCommand(null); + + GetConsumerStatusRequestHeader requestHeader = new GetConsumerStatusRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setGroup(group); + RemotingCommand request = + RemotingCommand.createRequestCommand(RequestCode.GET_CONSUMER_STATUS_FROM_CLIENT, + requestHeader); + + Map> consumerStatusTable = new HashMap<>(); + ConcurrentMap channelInfoTable = + this.brokerController.getConsumerManager().getConsumerGroupInfo(group).getChannelInfoTable(); + if (null == channelInfoTable || channelInfoTable.isEmpty()) { + result.setCode(ResponseCode.SYSTEM_ERROR); + result.setRemark(String.format("No Any Consumer online in the consumer group: [%s]", group)); + return result; + } + + for (Map.Entry entry : channelInfoTable.entrySet()) { + int version = entry.getValue().getVersion(); + String clientId = entry.getValue().getClientId(); + if (version < MQVersion.Version.V3_0_7_SNAPSHOT.ordinal()) { + 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. channel={}, version={}", + RemotingHelper.parseChannelRemoteAddr(entry.getKey()), MQVersion.getVersionDesc(version)); + return result; + } else if (UtilAll.isBlank(originClientId) || originClientId.equals(clientId)) { + try { + RemotingCommand response = + this.brokerController.getRemotingServer().invokeSync(entry.getKey(), request, 5000); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + if (response.getBody() != null) { + GetConsumerStatusBody body = + GetConsumerStatusBody.decode(response.getBody(), + GetConsumerStatusBody.class); + + consumerStatusTable.put(clientId, body.getMessageQueueTable()); + log.info( + "[get-consumer-status] get consumer status success. topic={}, group={}, channelRemoteAddr={}", + topic, group, clientId); + } + } + default: + break; + } + } catch (Exception e) { + log.error( + "[get-consumer-status] get consumer status exception. topic={}, group={}, error={}", + topic, group, e.toString()); + } + + if (!UtilAll.isBlank(originClientId) && originClientId.equals(clientId)) { + break; + } + } + } + + result.setCode(ResponseCode.SUCCESS); + GetConsumerStatusBody resBody = new GetConsumerStatusBody(); + resBody.setConsumerTable(consumerStatusTable); + result.setBody(resBody.encode()); + return result; + } +} 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 new file mode 100644 index 0000000..e00be4f --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/client/rebalance/RebalanceLockManager.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.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; + +public class RebalanceLockManager { + private static final Logger log = LoggerFactory.getLogger(LoggerName.REBALANCE_LOCK_LOGGER_NAME); + private final static long REBALANCE_LOCK_MAX_LIVE_TIME = Long.parseLong(System.getProperty( + "rocketmq.broker.rebalance.lockMaxLiveTime", "60000")); + private final Lock lock = new ReentrantLock(); + private final ConcurrentMap> mqLockTable = + 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) { + + if (!this.isLocked(group, mq, clientId)) { + try { + this.lock.lockInterruptibly(); + try { + ConcurrentHashMap groupValue = this.mqLockTable.get(group); + if (null == groupValue) { + groupValue = new ConcurrentHashMap<>(32); + this.mqLockTable.put(group, groupValue); + } + + LockEntry lockEntry = groupValue.get(mq); + if (null == lockEntry) { + lockEntry = new LockEntry(); + lockEntry.setClientId(clientId); + groupValue.put(mq, lockEntry); + log.info( + "RebalanceLockManager#tryLock: lock a message queue which has not been locked yet, " + + "group={}, clientId={}, mq={}", group, clientId, mq); + } + + if (lockEntry.isLocked(clientId)) { + lockEntry.setLastUpdateTimestamp(System.currentTimeMillis()); + return true; + } + + String oldClientId = lockEntry.getClientId(); + + if (lockEntry.isExpired()) { + lockEntry.setClientId(clientId); + lockEntry.setLastUpdateTimestamp(System.currentTimeMillis()); + log.warn( + "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( + "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("RebalanceLockManager#tryLock: unexpected error, group={}, mq={}, clientId={}", group, mq, + clientId, e); + } + } + + return true; + } + + private boolean isLocked(final String group, final MessageQueue mq, final String clientId) { + ConcurrentHashMap groupValue = this.mqLockTable.get(group); + if (groupValue != null) { + LockEntry lockEntry = groupValue.get(mq); + if (lockEntry != null) { + boolean locked = lockEntry.isLocked(clientId); + if (locked) { + lockEntry.setLastUpdateTimestamp(System.currentTimeMillis()); + } + + return locked; + } + } + + return false; + } + + public Set tryLockBatch(final String group, final Set mqs, + final String clientId) { + Set lockedMqs = new HashSet<>(mqs.size()); + Set notLockedMqs = new HashSet<>(mqs.size()); + + for (MessageQueue mq : mqs) { + if (this.isLocked(group, mq, clientId)) { + lockedMqs.add(mq); + } else { + notLockedMqs.add(mq); + } + } + + if (!notLockedMqs.isEmpty()) { + try { + this.lock.lockInterruptibly(); + try { + ConcurrentHashMap groupValue = this.mqLockTable.get(group); + if (null == groupValue) { + groupValue = new ConcurrentHashMap<>(32); + this.mqLockTable.put(group, groupValue); + } + + for (MessageQueue mq : notLockedMqs) { + LockEntry lockEntry = groupValue.get(mq); + if (null == lockEntry) { + lockEntry = new LockEntry(); + lockEntry.setClientId(clientId); + groupValue.put(mq, lockEntry); + log.info( + "RebalanceLockManager#tryLockBatch: lock a message which has not been locked yet, " + + "group={}, clientId={}, mq={}", group, clientId, mq); + } + + if (lockEntry.isLocked(clientId)) { + lockEntry.setLastUpdateTimestamp(System.currentTimeMillis()); + lockedMqs.add(mq); + continue; + } + + String oldClientId = lockEntry.getClientId(); + + if (lockEntry.isExpired()) { + lockEntry.setClientId(clientId); + lockEntry.setLastUpdateTimestamp(System.currentTimeMillis()); + log.warn( + "RebalanceLockManager#tryLockBatch: try to lock a expired message queue, group={}, " + + "mq={}, old client id={}, new client id={}", group, mq, oldClientId, clientId); + lockedMqs.add(mq); + continue; + } + + log.warn( + "RebalanceLockManager#tryLockBatch: message queue has been locked by other client, " + + "group={}, mq={}, locked client id={}, current client id={}", group, mq, oldClientId, + clientId); + } + } finally { + this.lock.unlock(); + } + } catch (InterruptedException e) { + log.error("RebalanceLockManager#tryBatch: unexpected error, group={}, mqs={}, clientId={}", group, mqs, + clientId, e); + } + } + + return lockedMqs; + } + + public void unlockBatch(final String group, final Set mqs, final String clientId) { + try { + this.lock.lockInterruptibly(); + try { + ConcurrentHashMap groupValue = this.mqLockTable.get(group); + if (null != groupValue) { + for (MessageQueue mq : mqs) { + LockEntry lockEntry = groupValue.get(mq); + if (null != lockEntry) { + if (lockEntry.getClientId().equals(clientId)) { + groupValue.remove(mq); + log.info("RebalanceLockManager#unlockBatch: unlock mq, group={}, clientId={}, mqs={}", + group, clientId, mq); + } else { + log.warn( + "RebalanceLockManager#unlockBatch: mq locked by other client, group={}, locked " + + "clientId={}, current clientId={}, mqs={}", group, lockEntry.getClientId(), + clientId, mq); + } + } else { + log.warn("RebalanceLockManager#unlockBatch: mq not locked, group={}, clientId={}, mq={}", + group, clientId, mq); + } + } + } else { + log.warn("RebalanceLockManager#unlockBatch: group not exist, group={}, clientId={}, mqs={}", group, + clientId, mqs); + } + } finally { + this.lock.unlock(); + } + } catch (InterruptedException e) { + log.error("RebalanceLockManager#unlockBatch: unexpected error, group={}, mqs={}, clientId={}", group, mqs, + clientId); + } + } + + static class LockEntry { + private String clientId; + private volatile long lastUpdateTimestamp = System.currentTimeMillis(); + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public long getLastUpdateTimestamp() { + return lastUpdateTimestamp; + } + + public void setLastUpdateTimestamp(long lastUpdateTimestamp) { + this.lastUpdateTimestamp = lastUpdateTimestamp; + } + + public boolean isLocked(final String clientId) { + boolean eq = this.clientId.equals(clientId); + return eq && !this.isExpired(); + } + + public boolean isExpired() { + boolean expired = + (System.currentTimeMillis() - this.lastUpdateTimestamp) > REBALANCE_LOCK_MAX_LIVE_TIME; + + return expired; + } + } +} 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 0000000..11fa0e7 --- /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 0000000..5b8b2fb --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/coldctr/ColdDataCgCtrService.java @@ -0,0 +1,249 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.fastjson2.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.isSysConsumerGroupPullMessage(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 0000000..c38d886 --- /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 0000000..87d9789 --- /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 0000000..f26a242 --- /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/config/v1/RocksDBConsumerOffsetManager.java b/broker/src/main/java/org/apache/rocketmq/broker/config/v1/RocksDBConsumerOffsetManager.java new file mode 100644 index 0000000..963c504 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/config/v1/RocksDBConsumerOffsetManager.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.broker.config.v1; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.serializer.SerializerFeature; +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.broker.RocksDBConfigManager; +import org.apache.rocketmq.broker.offset.ConsumerOffsetManager; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.constant.LoggerName; +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.protocol.DataVersion; +import org.rocksdb.CompressionType; +import org.rocksdb.WriteBatch; + +public class RocksDBConsumerOffsetManager extends ConsumerOffsetManager { + + protected static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + + protected transient RocksDBConfigManager rocksDBConfigManager; + + public RocksDBConsumerOffsetManager(BrokerController brokerController) { + super(brokerController); + this.rocksDBConfigManager = new RocksDBConfigManager(rocksdbConfigFilePath(), brokerController.getMessageStoreConfig().getMemTableFlushIntervalMs(), + CompressionType.getCompressionType(brokerController.getMessageStoreConfig().getRocksdbCompressionType())); + + } + + @Override + public boolean load() { + if (!rocksDBConfigManager.init()) { + return false; + } + if (!loadDataVersion() || !loadConsumerOffset()) { + return false; + } + + return true; + } + + public boolean loadConsumerOffset() { + return this.rocksDBConfigManager.loadData(this::decodeOffset) && merge(); + } + + private boolean merge() { + if (!UtilAll.isPathExists(this.configFilePath()) && !UtilAll.isPathExists(this.configFilePath() + ".bak")) { + log.info("consumerOffset json file does not exist, so skip merge"); + return true; + } + if (!super.loadDataVersion()) { + log.error("load json consumerOffset dataVersion error, startup will exit"); + return false; + } + + final DataVersion dataVersion = super.getDataVersion(); + final DataVersion kvDataVersion = this.getDataVersion(); + if (dataVersion.getCounter().get() > kvDataVersion.getCounter().get()) { + if (!super.load()) { + log.error("load json consumerOffset info failed, startup will exit"); + return false; + } + this.persist(); + this.getDataVersion().assignNewOne(dataVersion); + updateDataVersion(); + log.info("update offset from json, dataVersion:{}, offsetTable: {} ", this.getDataVersion(), JSON.toJSONString(this.getOffsetTable())); + } + return true; + } + + + @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); + } + } + + protected void decodeOffset(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()); + } + + public String rocksdbConfigFilePath() { + 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(); + } + } + + public synchronized void exportToJson() { + log.info("RocksDBConsumerOffsetManager export consumer offset to json file"); + super.persist(); + } + + 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); + } + + @Override + public boolean loadDataVersion() { + return this.rocksDBConfigManager.loadDataVersion(); + } + + @Override + public DataVersion getDataVersion() { + return rocksDBConfigManager.getKvDataVersion(); + } + + public void updateDataVersion() { + try { + rocksDBConfigManager.updateKvDataVersion(); + } catch (Exception e) { + log.error("update consumer offset dataVersion error", e); + throw new RuntimeException(e); + } + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/config/v1/RocksDBLmqSubscriptionGroupManager.java b/broker/src/main/java/org/apache/rocketmq/broker/config/v1/RocksDBLmqSubscriptionGroupManager.java new file mode 100644 index 0000000..05f3f7d --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/config/v1/RocksDBLmqSubscriptionGroupManager.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.config.v1; + +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); + } + + @Override + public boolean containsSubscriptionGroup(String group) { + if (MixAll.isLmq(group)) { + return true; + } else { + return super.containsSubscriptionGroup(group); + } + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/config/v1/RocksDBLmqTopicConfigManager.java b/broker/src/main/java/org/apache/rocketmq/broker/config/v1/RocksDBLmqTopicConfigManager.java new file mode 100644 index 0000000..7b27801 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/config/v1/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.config.v1; + +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/config/v1/RocksDBOffsetSerializeWrapper.java b/broker/src/main/java/org/apache/rocketmq/broker/config/v1/RocksDBOffsetSerializeWrapper.java new file mode 100644 index 0000000..4801cfc --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/config/v1/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.config.v1; + +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/config/v1/RocksDBSubscriptionGroupManager.java b/broker/src/main/java/org/apache/rocketmq/broker/config/v1/RocksDBSubscriptionGroupManager.java new file mode 100644 index 0000000..b208169 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/config/v1/RocksDBSubscriptionGroupManager.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.broker.config.v1; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.serializer.SerializerFeature; +import java.io.File; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.BiConsumer; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.RocksDBConfigManager; +import org.apache.rocketmq.broker.subscription.SubscriptionGroupManager; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.utils.DataConverter; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.rocksdb.CompressionType; +import org.rocksdb.RocksIterator; + +public class RocksDBSubscriptionGroupManager extends SubscriptionGroupManager { + + protected transient RocksDBConfigManager rocksDBConfigManager; + + public RocksDBSubscriptionGroupManager(BrokerController brokerController) { + super(brokerController, false); + this.rocksDBConfigManager = new RocksDBConfigManager(rocksdbConfigFilePath(), brokerController.getMessageStoreConfig().getMemTableFlushIntervalMs(), + CompressionType.getCompressionType(brokerController.getMessageStoreConfig().getRocksdbCompressionType())); + } + + @Override + public boolean load() { + if (!rocksDBConfigManager.init()) { + return false; + } + if (!loadDataVersion() || !loadSubscriptionGroupAndForbidden()) { + return false; + } + this.init(); + return true; + } + + public boolean loadDataVersion() { + return this.rocksDBConfigManager.loadDataVersion(); + } + + public boolean loadSubscriptionGroupAndForbidden() { + return this.rocksDBConfigManager.loadData(this::decodeSubscriptionGroup) + && this.loadForbidden(this::decodeForbidden) + && merge(); + } + + public boolean loadForbidden(BiConsumer biConsumer) { + try (RocksIterator iterator = this.rocksDBConfigManager.configRocksDBStorage.forbiddenIterator()) { + iterator.seekToFirst(); + while (iterator.isValid()) { + biConsumer.accept(iterator.key(), iterator.value()); + iterator.next(); + } + } + return true; + } + + private boolean merge() { + if (!UtilAll.isPathExists(this.configFilePath()) && !UtilAll.isPathExists(this.configFilePath() + ".bak")) { + log.info("subGroup json file does not exist, so skip merge"); + return true; + } + if (!super.loadDataVersion()) { + log.error("load json subGroup dataVersion error, startup will exit"); + return false; + } + final DataVersion dataVersion = super.getDataVersion(); + final DataVersion kvDataVersion = this.getDataVersion(); + if (dataVersion.getCounter().get() > kvDataVersion.getCounter().get()) { + if (!super.load()) { + log.error("load group and forbidden info from json file error, startup will exit"); + return false; + } + final ConcurrentMap groupTable = this.getSubscriptionGroupTable(); + for (Map.Entry entry : groupTable.entrySet()) { + putSubscriptionGroupConfig(entry.getValue()); + log.info("import subscription config to rocksdb, group={}", entry.getValue()); + } + final ConcurrentMap> forbiddenTable = this.getForbiddenTable(); + for (Map.Entry> entry : forbiddenTable.entrySet()) { + try { + this.rocksDBConfigManager.updateForbidden(entry.getKey(), JSON.toJSONString(entry.getValue())); + log.info("import forbidden config to rocksdb, group={}", entry.getValue()); + } catch (Exception e) { + log.error("import forbidden config to rocksdb failed, group={}", entry.getValue()); + return false; + } + } + this.getDataVersion().assignNewOne(dataVersion); + updateDataVersion(); + } else { + log.info("dataVersion is not greater than kvDataVersion, no need to merge group metaData, dataVersion={}, kvDataVersion={}", dataVersion, kvDataVersion); + } + log.info("finish marge subscription config from json file and merge to rocksdb"); + this.persist(); + + return true; + } + + @Override + public boolean stop() { + return this.rocksDBConfigManager.stop(); + } + + @Override + public 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; + } + + + protected void decodeSubscriptionGroup(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(); + } + } + + public synchronized void exportToJson() { + log.info("RocksDBSubscriptionGroupManager export subscription group to json file"); + super.persist(); + } + + public String rocksdbConfigFilePath() { + return this.brokerController.getMessageStoreConfig().getStorePathRootDir() + File.separator + "config" + File.separator + "subscriptionGroups" + File.separator; + } + + @Override + public DataVersion getDataVersion() { + return rocksDBConfigManager.getKvDataVersion(); + } + + @Override + public void updateDataVersion() { + try { + rocksDBConfigManager.updateKvDataVersion(); + } catch (Exception e) { + log.error("update group config dataVersion error", e); + throw new RuntimeException(e); + } + } + + protected void decodeForbidden(byte[] key, byte[] body) { + String forbiddenGroupName = new String(key, DataConverter.CHARSET_UTF8); + JSONObject jsonObject = JSON.parseObject(new String(body, DataConverter.CHARSET_UTF8)); + Set> entries = jsonObject.entrySet(); + ConcurrentMap forbiddenGroup = new ConcurrentHashMap<>(entries.size()); + for (Map.Entry entry : entries) { + forbiddenGroup.put(entry.getKey(), (Integer) entry.getValue()); + } + this.getForbiddenTable().put(forbiddenGroupName, forbiddenGroup); + log.info("load forbidden,{} value {}", forbiddenGroupName, forbiddenGroup.toString()); + } + + @Override + public void updateForbidden(String group, String topic, int forbiddenIndex, boolean setOrClear) { + try { + super.updateForbidden(group, topic, forbiddenIndex, setOrClear); + this.rocksDBConfigManager.updateForbidden(group, JSON.toJSONString(this.getForbiddenTable().get(group))); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void setForbidden(String group, String topic, int forbiddenIndex) { + try { + super.setForbidden(group, topic, forbiddenIndex); + this.rocksDBConfigManager.updateForbidden(group, JSON.toJSONString(this.getForbiddenTable().get(group))); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void clearForbidden(String group, String topic, int forbiddenIndex) { + try { + super.clearForbidden(group, topic, forbiddenIndex); + this.rocksDBConfigManager.updateForbidden(group, JSON.toJSONString(this.getForbiddenTable().get(group))); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/config/v1/RocksDBTopicConfigManager.java b/broker/src/main/java/org/apache/rocketmq/broker/config/v1/RocksDBTopicConfigManager.java new file mode 100644 index 0000000..d64f808 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/config/v1/RocksDBTopicConfigManager.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.broker.config.v1; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.serializer.SerializerFeature; +import java.io.File; +import java.util.Map; +import java.util.concurrent.ConcurrentMap; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.RocksDBConfigManager; +import org.apache.rocketmq.broker.topic.TopicConfigManager; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.utils.DataConverter; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.rocksdb.CompressionType; + +public class RocksDBTopicConfigManager extends TopicConfigManager { + + protected transient RocksDBConfigManager rocksDBConfigManager; + + public RocksDBTopicConfigManager(BrokerController brokerController) { + super(brokerController, false); + this.rocksDBConfigManager = new RocksDBConfigManager(rocksdbConfigFilePath(), brokerController.getMessageStoreConfig().getMemTableFlushIntervalMs(), + CompressionType.getCompressionType(brokerController.getMessageStoreConfig().getRocksdbCompressionType())); + } + + @Override + public boolean load() { + if (!rocksDBConfigManager.init()) { + return false; + } + if (!loadDataVersion() || !loadTopicConfig()) { + return false; + } + this.init(); + return true; + } + + public boolean loadTopicConfig() { + return this.rocksDBConfigManager.loadData(this::decodeTopicConfig) && merge(); + } + + public boolean loadDataVersion() { + return this.rocksDBConfigManager.loadDataVersion(); + } + + private boolean merge() { + if (!UtilAll.isPathExists(this.configFilePath()) && !UtilAll.isPathExists(this.configFilePath() + ".bak")) { + log.info("topic json file does not exist, so skip merge"); + return true; + } + + if (!super.loadDataVersion()) { + log.error("load json topic dataVersion error, startup will exit"); + return false; + } + + final DataVersion dataVersion = super.getDataVersion(); + final DataVersion kvDataVersion = this.getDataVersion(); + if (dataVersion.getCounter().get() > kvDataVersion.getCounter().get()) { + if (!super.load()) { + log.error("load topic config from json file error, startup will exit"); + return false; + } + final ConcurrentMap topicConfigTable = this.getTopicConfigTable(); + for (Map.Entry entry : topicConfigTable.entrySet()) { + putTopicConfig(entry.getValue()); + log.info("import topic config to rocksdb, topic={}", entry.getValue()); + } + this.getDataVersion().assignNewOne(dataVersion); + updateDataVersion(); + } else { + log.info("dataVersion is not greater than kvDataVersion, no need to merge topic metaData, dataVersion={}, kvDataVersion={}", dataVersion, kvDataVersion); + } + log.info("finish read topic config from json file and merge to rocksdb"); + this.persist(); + return true; + } + + + @Override + public boolean stop() { + return this.rocksDBConfigManager.stop(); + } + + protected void decodeTopicConfig(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 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(); + } + } + + public synchronized void exportToJson() { + log.info("RocksDBTopicConfigManager export topic config to json file"); + super.persist(); + } + + public String rocksdbConfigFilePath() { + return this.brokerController.getMessageStoreConfig().getStorePathRootDir() + File.separator + "config" + File.separator + "topics" + File.separator; + } + + + @Override + public DataVersion getDataVersion() { + return rocksDBConfigManager.getKvDataVersion(); + } + + @Override + public void updateDataVersion() { + try { + rocksDBConfigManager.updateKvDataVersion(); + } catch (Exception e) { + log.error("update topic config dataVersion error", e); + throw new RuntimeException(e); + } + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/config/v2/ConfigHelper.java b/broker/src/main/java/org/apache/rocketmq/broker/config/v2/ConfigHelper.java new file mode 100644 index 0000000..29a7c31 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/config/v2/ConfigHelper.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.broker.config.v2; + +import com.alibaba.fastjson2.JSON; +import com.google.common.base.Preconditions; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.common.config.AbstractRocksDBStorage; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.rocksdb.RocksDBException; +import org.rocksdb.WriteBatch; + +public class ConfigHelper { + + /** + *

+ * Layout of data version key: + * [table-prefix, 1 byte][table-id, 2 byte][record-prefix, 1 byte][data-version-bytes] + *

+ * + *

+ * Layout of data version value: + * [state-machine-version, 8 bytes][timestamp, 8 bytes][sequence counter, 8 bytes] + *

+ * + * @throws RocksDBException if RocksDB raises an error + */ + public static Optional loadDataVersion(ConfigStorage configStorage, TableId tableId) + throws RocksDBException { + int keyLen = 1 /* table-prefix */ + Short.BYTES /* table-id */ + 1 /* record-prefix */ + + ConfigStorage.DATA_VERSION_KEY_BYTES.length; + ByteBuf keyBuf = AbstractRocksDBStorage.POOLED_ALLOCATOR.buffer(keyLen); + try { + keyBuf.writeByte(TablePrefix.TABLE.getValue()); + keyBuf.writeShort(tableId.getValue()); + keyBuf.writeByte(RecordPrefix.DATA_VERSION.getValue()); + keyBuf.writeBytes(ConfigStorage.DATA_VERSION_KEY_BYTES); + byte[] valueByes = configStorage.get(keyBuf.nioBuffer()); + if (null != valueByes) { + ByteBuf valueBuf = Unpooled.wrappedBuffer(valueByes); + return Optional.of(valueBuf); + } + } finally { + keyBuf.release(); + } + return Optional.empty(); + } + + public static void stampDataVersion(WriteBatch writeBatch, TableId table, DataVersion dataVersion, long stateMachineVersion) + throws RocksDBException { + // Increase data version + dataVersion.nextVersion(stateMachineVersion); + + int keyLen = 1 /* table-prefix */ + Short.BYTES /* table-id */ + 1 /* record-prefix */ + + ConfigStorage.DATA_VERSION_KEY_BYTES.length; + ByteBuf keyBuf = AbstractRocksDBStorage.POOLED_ALLOCATOR.buffer(keyLen); + ByteBuf valueBuf = AbstractRocksDBStorage.POOLED_ALLOCATOR.buffer(Long.BYTES * 3); + try { + keyBuf.writeByte(TablePrefix.TABLE.getValue()); + keyBuf.writeShort(table.getValue()); + keyBuf.writeByte(RecordPrefix.DATA_VERSION.getValue()); + keyBuf.writeBytes(ConfigStorage.DATA_VERSION_KEY_BYTES); + valueBuf.writeLong(dataVersion.getStateVersion()); + valueBuf.writeLong(dataVersion.getTimestamp()); + valueBuf.writeLong(dataVersion.getCounter().get()); + writeBatch.put(keyBuf.nioBuffer(), valueBuf.nioBuffer()); + } finally { + keyBuf.release(); + valueBuf.release(); + } + } + + public static void onDataVersionLoad(ByteBuf buf, DataVersion dataVersion) { + if (buf.readableBytes() == 8 /* state machine version */ + 8 /* timestamp */ + 8 /* counter */) { + long stateMachineVersion = buf.readLong(); + long timestamp = buf.readLong(); + long counter = buf.readLong(); + dataVersion.setStateVersion(stateMachineVersion); + dataVersion.setTimestamp(timestamp); + dataVersion.setCounter(new AtomicLong(counter)); + } + buf.release(); + } + + public static ByteBuf keyBufOf(TableId tableId, final String name) { + Preconditions.checkNotNull(name); + byte[] bytes = name.getBytes(StandardCharsets.UTF_8); + int keyLen = 1 /* table-prefix */ + 2 /* table-id */ + 1 /* record-type-prefix */ + 2 /* name-length */ + bytes.length; + ByteBuf keyBuf = AbstractRocksDBStorage.POOLED_ALLOCATOR.buffer(keyLen); + keyBuf.writeByte(TablePrefix.TABLE.getValue()); + keyBuf.writeShort(tableId.getValue()); + keyBuf.writeByte(RecordPrefix.DATA.getValue()); + keyBuf.writeShort(bytes.length); + keyBuf.writeBytes(bytes); + return keyBuf; + } + + public static ByteBuf valueBufOf(final Object config, SerializationType serializationType) { + if (SerializationType.JSON == serializationType) { + byte[] payload = JSON.toJSONBytes(config); + ByteBuf valueBuf = AbstractRocksDBStorage.POOLED_ALLOCATOR.buffer(1 + payload.length); + valueBuf.writeByte(SerializationType.JSON.getValue()); + valueBuf.writeBytes(payload); + return valueBuf; + } + throw new RuntimeException("Unsupported serialization type: " + serializationType); + } + + public static byte[] readBytes(final ByteBuf buf) { + byte[] bytes = new byte[buf.readableBytes()]; + buf.readBytes(bytes); + return bytes; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/config/v2/ConfigStorage.java b/broker/src/main/java/org/apache/rocketmq/broker/config/v2/ConfigStorage.java new file mode 100644 index 0000000..c4056d1 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/config/v2/ConfigStorage.java @@ -0,0 +1,274 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.config.v2; + +import com.google.common.base.Stopwatch; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import io.netty.buffer.PooledByteBufAllocatorMetric; +import io.netty.util.internal.PlatformDependent; +import java.io.File; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.config.AbstractRocksDBStorage; +import org.apache.rocketmq.common.config.ConfigHelper; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.rocksdb.ColumnFamilyDescriptor; +import org.rocksdb.ColumnFamilyOptions; +import org.rocksdb.FlushOptions; +import org.rocksdb.ReadOptions; +import org.rocksdb.RocksDB; +import org.rocksdb.RocksDBException; +import org.rocksdb.RocksIterator; +import org.rocksdb.Slice; +import org.rocksdb.WriteBatch; +import org.rocksdb.WriteOptions; + +/** + * https://book.tidb.io/session1/chapter3/tidb-kv-to-relation.html + */ +public class ConfigStorage extends AbstractRocksDBStorage { + + public static final String DATA_VERSION_KEY = "data_version"; + public static final byte[] DATA_VERSION_KEY_BYTES = DATA_VERSION_KEY.getBytes(StandardCharsets.UTF_8); + + private final ScheduledExecutorService scheduledExecutorService; + + /** + * Number of write ops since previous flush. + */ + private final AtomicInteger writeOpsCounter; + + private final AtomicLong estimateWalFileSize = new AtomicLong(0L); + + private final MessageStoreConfig messageStoreConfig; + + private final FlushSyncService flushSyncService; + + public ConfigStorage(MessageStoreConfig messageStoreConfig) { + super(messageStoreConfig.getStorePathRootDir() + File.separator + "config" + File.separator + "rdb"); + this.messageStoreConfig = messageStoreConfig; + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat("config-storage-%d") + .build(); + scheduledExecutorService = new ScheduledThreadPoolExecutor(1, threadFactory); + writeOpsCounter = new AtomicInteger(0); + this.flushSyncService = new FlushSyncService(); + this.flushSyncService.setDaemon(true); + } + + private void statNettyMemory() { + PooledByteBufAllocatorMetric metric = AbstractRocksDBStorage.POOLED_ALLOCATOR.metric(); + LOGGER.info("Netty Memory Usage: {}", metric); + } + + @Override + public synchronized boolean start() { + boolean started = super.start(); + if (started) { + scheduledExecutorService.scheduleWithFixedDelay(() -> statRocksdb(LOGGER), 1, 10, TimeUnit.SECONDS); + scheduledExecutorService.scheduleWithFixedDelay(this::statNettyMemory, 10, 10, TimeUnit.SECONDS); + this.flushSyncService.start(); + } else { + LOGGER.error("Failed to start config storage"); + } + return started; + } + + @Override + protected boolean postLoad() { + if (!PlatformDependent.hasUnsafe()) { + LOGGER.error("Unsafe not available and POOLED_ALLOCATOR cannot work correctly"); + return false; + } + try { + UtilAll.ensureDirOK(this.dbPath); + initOptions(); + List cfDescriptors = new ArrayList<>(); + + ColumnFamilyOptions defaultOptions = ConfigHelper.createConfigColumnFamilyOptions(); + this.cfOptions.add(defaultOptions); + cfDescriptors.add(new ColumnFamilyDescriptor(RocksDB.DEFAULT_COLUMN_FAMILY, defaultOptions)); + + // Start RocksDB instance + open(cfDescriptors); + + this.defaultCFHandle = cfHandles.get(0); + } catch (Exception e) { + AbstractRocksDBStorage.LOGGER.error("postLoad Failed. {}", this.dbPath, e); + return false; + } + return true; + } + + @Override + protected void preShutdown() { + scheduledExecutorService.shutdown(); + flushSyncService.shutdown(); + } + + protected void initOptions() { + this.options = ConfigHelper.createConfigDBOptions(); + super.initOptions(); + } + + @Override + protected void initAbleWalWriteOptions() { + this.ableWalWriteOptions = new WriteOptions(); + + // Given that fdatasync is kind of expensive, sync-WAL for every write cannot be afforded. + this.ableWalWriteOptions.setSync(false); + + // We need WAL for config changes + this.ableWalWriteOptions.setDisableWAL(false); + + // No fast failure on block, wait synchronously even if there is wait for the write request + this.ableWalWriteOptions.setNoSlowdown(false); + } + + public byte[] get(ByteBuffer key) throws RocksDBException { + byte[] keyBytes = new byte[key.remaining()]; + key.get(keyBytes); + return super.get(getDefaultCFHandle(), totalOrderReadOptions, keyBytes); + } + + public void write(WriteBatch writeBatch) throws RocksDBException { + db.write(ableWalWriteOptions, writeBatch); + accountWriteOps(writeBatch.getDataSize()); + } + + private void accountWriteOps(long dataSize) { + writeOpsCounter.incrementAndGet(); + estimateWalFileSize.addAndGet(dataSize); + } + + public RocksIterator iterate(ByteBuffer beginKey, ByteBuffer endKey) { + try (ReadOptions readOptions = new ReadOptions()) { + readOptions.setTotalOrderSeek(true); + readOptions.setTailing(false); + readOptions.setAutoPrefixMode(true); + // Use DirectSlice till the follow issue is fixed: + // https://github.com/facebook/rocksdb/issues/13098 + // + // readOptions.setIterateUpperBound(new DirectSlice(endKey)); + byte[] buf = new byte[endKey.remaining()]; + endKey.slice().get(buf); + readOptions.setIterateUpperBound(new Slice(buf)); + + RocksIterator iterator = db.newIterator(defaultCFHandle, readOptions); + iterator.seek(beginKey.slice()); + return iterator; + } + } + + /** + * RocksDB writes contain 3 stages: application memory buffer --> OS Page Cache --> Disk. + * Given that we are having DBOptions::manual_wal_flush, we need to manually call DB::FlushWAL and DB::SyncWAL + * Note: DB::FlushWAL(true) will internally call DB::SyncWAL. + *

+ * See Flush And Sync WAL + */ + class FlushSyncService extends ServiceThread { + + private long lastSyncTime = 0; + + private static final long MAX_SYNC_INTERVAL_IN_MILLIS = 100; + + private final Stopwatch stopwatch = Stopwatch.createUnstarted(); + + private final FlushOptions flushOptions = new FlushOptions(); + + @Override + public String getServiceName() { + return "FlushSyncService"; + } + + @Override + public void run() { + flushOptions.setAllowWriteStall(false); + flushOptions.setWaitForFlush(true); + log.info("{} service started", this.getServiceName()); + while (!this.isStopped()) { + try { + this.waitForRunning(10); + this.flushAndSyncWAL(false); + } catch (Exception e) { + log.warn("{} service has exception. ", this.getServiceName(), e); + } + } + try { + flushAndSyncWAL(true); + } catch (Exception e) { + log.warn("{} raised an exception while performing flush-and-sync WAL on exit", + this.getServiceName(), e); + } + flushOptions.close(); + log.info("{} service end", this.getServiceName()); + } + + private void flushAndSyncWAL(boolean onExit) throws RocksDBException { + int writeOps = writeOpsCounter.get(); + if (0 == writeOps) { + // No write ops to flush + return; + } + + /* + * Normally, when MemTables become full then immutable, RocksDB threads will automatically flush them to L0 + * SST files. The use case here is different: the MemTable may never get full and immutable given that the + * volume of data involved is relatively small. Further, we are constantly modifying the key-value pairs and + * generating WAL entries. The WAL file size can grow up to dozens of gigabytes without manual triggering of + * flush. + */ + if (ConfigStorage.this.estimateWalFileSize.get() >= messageStoreConfig.getRocksdbWalFileRollingThreshold()) { + ConfigStorage.this.flush(flushOptions); + estimateWalFileSize.set(0L); + } + + // Flush and Sync WAL if we have committed enough writes + if (writeOps >= messageStoreConfig.getRocksdbFlushWalFrequency() || onExit) { + stopwatch.reset().start(); + ConfigStorage.this.db.flushWal(true); + long elapsed = stopwatch.stop().elapsed(TimeUnit.MILLISECONDS); + writeOpsCounter.getAndAdd(-writeOps); + lastSyncTime = System.currentTimeMillis(); + LOGGER.debug("Flush and Sync WAL of RocksDB[{}] costs {}ms, write-ops={}", dbPath, elapsed, writeOps); + return; + } + // Flush and Sync WAL if some writes are out there for a period of time + long elapsedTime = System.currentTimeMillis() - lastSyncTime; + if (elapsedTime > MAX_SYNC_INTERVAL_IN_MILLIS) { + stopwatch.reset().start(); + ConfigStorage.this.db.flushWal(true); + long elapsed = stopwatch.stop().elapsed(TimeUnit.MILLISECONDS); + LOGGER.debug("Flush and Sync WAL of RocksDB[{}] costs {}ms, write-ops={}", dbPath, elapsed, writeOps); + writeOpsCounter.getAndAdd(-writeOps); + lastSyncTime = System.currentTimeMillis(); + } + } + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/config/v2/ConsumerOffsetManagerV2.java b/broker/src/main/java/org/apache/rocketmq/broker/config/v2/ConsumerOffsetManagerV2.java new file mode 100644 index 0000000..1821c80 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/config/v2/ConsumerOffsetManagerV2.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.broker.config.v2; + +import io.netty.buffer.ByteBuf; +import io.netty.util.internal.PlatformDependent; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.offset.ConsumerOffsetManager; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.config.AbstractRocksDBStorage; +import org.apache.rocketmq.store.MessageStore; +import org.rocksdb.RocksDBException; +import org.rocksdb.RocksIterator; +import org.rocksdb.WriteBatch; + +/** + *

+ * Layout of consumer offset key: + * [table-prefix, 1 byte][table-id, 2 bytes][record-prefix, 1 byte][group-len, 2 bytes][group bytes][CTRL_1, 1 byte] + * [topic-len, 2 bytes][topic bytes][CTRL_1, 1 byte][queue-id, 4 bytes] + *

+ * + *

+ * Layout of consumer offset value: [offset, 8 bytes] + *

+ */ +public class ConsumerOffsetManagerV2 extends ConsumerOffsetManager { + + private final ConfigStorage configStorage; + + public ConsumerOffsetManagerV2(BrokerController brokerController, ConfigStorage configStorage) { + super(brokerController); + this.configStorage = configStorage; + } + + @Override + protected void removeConsumerOffset(String topicAtGroup) { + if (!MixAll.isLmq(topicAtGroup)) { + super.removeConsumerOffset(topicAtGroup); + } + + String[] topicGroup = topicAtGroup.split(TOPIC_GROUP_SEPARATOR); + if (topicGroup.length != 2) { + LOG.error("Invalid topic group: {}", topicAtGroup); + return; + } + + byte[] topicBytes = topicGroup[0].getBytes(StandardCharsets.UTF_8); + byte[] groupBytes = topicGroup[1].getBytes(StandardCharsets.UTF_8); + + int keyLen = 1 /* table-prefix */ + Short.BYTES /* table-id */ + 1 /* record-prefix */ + + Short.BYTES /* group-len */ + groupBytes.length + 1 /* CTRL_1 */ + + Short.BYTES + topicBytes.length + 1; + // [table-prefix, 1 byte][table-id, 2 bytes][record-prefix, 1 byte][group-len, 2 bytes][group-bytes][CTRL_1, 1 byte] + // [topic-len, 2 bytes][topic-bytes][CTRL_1] + ByteBuf beginKey = AbstractRocksDBStorage.POOLED_ALLOCATOR.buffer(keyLen); + beginKey.writeByte(TablePrefix.TABLE.getValue()); + beginKey.writeShort(TableId.CONSUMER_OFFSET.getValue()); + beginKey.writeByte(RecordPrefix.DATA.getValue()); + beginKey.writeShort(groupBytes.length); + beginKey.writeBytes(groupBytes); + beginKey.writeByte(AbstractRocksDBStorage.CTRL_1); + beginKey.writeShort(topicBytes.length); + beginKey.writeBytes(topicBytes); + beginKey.writeByte(AbstractRocksDBStorage.CTRL_1); + + ByteBuf endKey = AbstractRocksDBStorage.POOLED_ALLOCATOR.buffer(keyLen); + endKey.writeByte(TablePrefix.TABLE.getValue()); + endKey.writeShort(TableId.CONSUMER_OFFSET.getValue()); + endKey.writeByte(RecordPrefix.DATA.getValue()); + endKey.writeShort(groupBytes.length); + endKey.writeBytes(groupBytes); + endKey.writeByte(AbstractRocksDBStorage.CTRL_1); + endKey.writeShort(topicBytes.length); + endKey.writeBytes(topicBytes); + endKey.writeByte(AbstractRocksDBStorage.CTRL_2); + + try (WriteBatch writeBatch = new WriteBatch()) { + // TODO: we have to make a copy here as WriteBatch lacks ByteBuffer API here + writeBatch.deleteRange(ConfigHelper.readBytes(beginKey), ConfigHelper.readBytes(endKey)); + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + ConfigHelper.stampDataVersion(writeBatch, TableId.CONSUMER_OFFSET, dataVersion, stateMachineVersion); + configStorage.write(writeBatch); + } catch (RocksDBException e) { + LOG.error("Failed to removeConsumerOffset, topicAtGroup={}", topicAtGroup, e); + } finally { + beginKey.release(); + endKey.release(); + } + } + + @Override + public void removeOffset(String group) { + if (!MixAll.isLmq(group)) { + super.removeOffset(group); + } + + byte[] groupBytes = group.getBytes(StandardCharsets.UTF_8); + int keyLen = 1 /* table-prefix */ + Short.BYTES /* table-id */ + 1 /* record-prefix */ + + Short.BYTES /* group-len */ + groupBytes.length + 1 /* CTRL_1 */; + + // [table-prefix, 1 byte][table-id, 2 bytes][record-prefix, 1 byte][group-len, 2 bytes][group bytes][CTRL_1, 1 byte] + ByteBuf beginKey = AbstractRocksDBStorage.POOLED_ALLOCATOR.buffer(keyLen); + beginKey.writeByte(TablePrefix.TABLE.getValue()); + beginKey.writeShort(TableId.CONSUMER_OFFSET.getValue()); + beginKey.writeByte(RecordPrefix.DATA.getValue()); + beginKey.writeShort(groupBytes.length); + beginKey.writeBytes(groupBytes); + beginKey.writeByte(AbstractRocksDBStorage.CTRL_1); + + ByteBuf endKey = AbstractRocksDBStorage.POOLED_ALLOCATOR.buffer(keyLen); + endKey.writeByte(TablePrefix.TABLE.getValue()); + endKey.writeShort(TableId.CONSUMER_OFFSET.getValue()); + endKey.writeByte(RecordPrefix.DATA.getValue()); + endKey.writeShort(groupBytes.length); + endKey.writeBytes(groupBytes); + endKey.writeByte(AbstractRocksDBStorage.CTRL_2); + try (WriteBatch writeBatch = new WriteBatch()) { + // TODO: we have to make a copy here as WriteBatch lacks ByteBuffer API here + writeBatch.deleteRange(ConfigHelper.readBytes(beginKey), ConfigHelper.readBytes(endKey)); + MessageStore messageStore = brokerController.getMessageStore(); + long stateMachineVersion = messageStore != null ? messageStore.getStateMachineVersion() : 0; + ConfigHelper.stampDataVersion(writeBatch, TableId.CONSUMER_OFFSET, dataVersion, stateMachineVersion); + configStorage.write(writeBatch); + } catch (RocksDBException e) { + LOG.error("Failed to consumer offsets by group={}", group, e); + } finally { + beginKey.release(); + endKey.release(); + } + } + + /** + *

+ * Layout of consumer offset key: + * [table-prefix, 1 byte][table-id, 2 bytes][record-prefix, 1 byte][group-len, 2 bytes][group bytes][CTRL_1, 1 byte] + * [topic-len, 2 bytes][topic bytes][CTRL_1, 1 byte][queue-id, 4 bytes] + *

+ * + *

+ * Layout of consumer offset value: + * [offset, 8 bytes] + *

+ * + * @param clientHost The client that submits consumer offsets + * @param group Group name + * @param topic Topic name + * @param queueId Queue ID + * @param offset Consumer offset of the specified queue + */ + @Override + public void commitOffset(String clientHost, String group, String topic, int queueId, long offset) { + String key = topic + TOPIC_GROUP_SEPARATOR + group; + + // We maintain a copy of classic consumer offset table in memory as they take very limited memory footprint. + // For LMQ offsets, given the volume and number of these type of records, they are maintained in RocksDB + // directly. Frequently used LMQ consumer offsets should reside either in block-cache or MemTable, so read/write + // should be blazingly fast. + if (!MixAll.isLmq(topic)) { + if (offsetTable.containsKey(key)) { + offsetTable.get(key).put(queueId, offset); + } else { + ConcurrentMap map = new ConcurrentHashMap<>(); + ConcurrentMap prev = offsetTable.putIfAbsent(key, map); + if (null != prev) { + map = prev; + } + map.put(queueId, offset); + } + } + + ByteBuf keyBuf = keyOfConsumerOffset(group, topic, queueId); + ByteBuf valueBuf = ConfigStorage.POOLED_ALLOCATOR.buffer(Long.BYTES); + try (WriteBatch writeBatch = new WriteBatch()) { + valueBuf.writeLong(offset); + writeBatch.put(keyBuf.nioBuffer(), valueBuf.nioBuffer()); + MessageStore messageStore = brokerController.getMessageStore(); + long stateMachineVersion = messageStore != null ? messageStore.getStateMachineVersion() : 0; + ConfigHelper.stampDataVersion(writeBatch, TableId.CONSUMER_OFFSET, dataVersion, stateMachineVersion); + configStorage.write(writeBatch); + } catch (RocksDBException e) { + LOG.error("Failed to commit consumer offset", e); + } finally { + keyBuf.release(); + valueBuf.release(); + } + } + + private ByteBuf keyOfConsumerOffset(String group, String topic, int queueId) { + byte[] groupBytes = group.getBytes(StandardCharsets.UTF_8); + byte[] topicBytes = topic.getBytes(StandardCharsets.UTF_8); + int keyLen = 1 /*table prefix*/ + Short.BYTES /*table-id*/ + 1 /*record-prefix*/ + + Short.BYTES /*group-len*/ + groupBytes.length + 1 /*CTRL_1*/ + + 2 /*topic-len*/ + topicBytes.length + 1 /* CTRL_1*/ + + Integer.BYTES /*queue-id*/; + ByteBuf keyBuf = ConfigStorage.POOLED_ALLOCATOR.buffer(keyLen); + keyBuf.writeByte(TablePrefix.TABLE.getValue()); + keyBuf.writeShort(TableId.CONSUMER_OFFSET.getValue()); + keyBuf.writeByte(RecordPrefix.DATA.getValue()); + keyBuf.writeShort(groupBytes.length); + keyBuf.writeBytes(groupBytes); + keyBuf.writeByte(AbstractRocksDBStorage.CTRL_1); + keyBuf.writeShort(topicBytes.length); + keyBuf.writeBytes(topicBytes); + keyBuf.writeByte(AbstractRocksDBStorage.CTRL_1); + keyBuf.writeInt(queueId); + return keyBuf; + } + + private ByteBuf keyOfPullOffset(String group, String topic, int queueId) { + byte[] groupBytes = group.getBytes(StandardCharsets.UTF_8); + byte[] topicBytes = topic.getBytes(StandardCharsets.UTF_8); + int keyLen = 1 /*table prefix*/ + Short.BYTES /*table-id*/ + 1 /*record-prefix*/ + + Short.BYTES /*group-len*/ + groupBytes.length + 1 /*CTRL_1*/ + + 2 /*topic-len*/ + topicBytes.length + 1 /* CTRL_1*/ + + Integer.BYTES /*queue-id*/; + ByteBuf keyBuf = ConfigStorage.POOLED_ALLOCATOR.buffer(keyLen); + keyBuf.writeByte(TablePrefix.TABLE.getValue()); + keyBuf.writeShort(TableId.PULL_OFFSET.getValue()); + keyBuf.writeByte(RecordPrefix.DATA.getValue()); + keyBuf.writeShort(groupBytes.length); + keyBuf.writeBytes(groupBytes); + keyBuf.writeByte(AbstractRocksDBStorage.CTRL_1); + keyBuf.writeShort(topicBytes.length); + keyBuf.writeBytes(topicBytes); + keyBuf.writeByte(AbstractRocksDBStorage.CTRL_1); + keyBuf.writeInt(queueId); + return keyBuf; + } + + @Override + public boolean load() { + return loadDataVersion() && loadConsumerOffsets(); + } + + @Override + public synchronized void persist() { + try { + configStorage.flushWAL(); + } catch (RocksDBException e) { + LOG.error("Failed to flush RocksDB config instance WAL", e); + } + } + + /** + *

+ * Layout of data version key: + * [table-prefix, 1 byte][table-id, 2 byte][record-prefix, 1 byte][data-version-bytes] + *

+ * + *

+ * Layout of data version value: + * [state-machine-version, 8 bytes][timestamp, 8 bytes][sequence counter, 8 bytes] + *

+ */ + public boolean loadDataVersion() { + try { + ConfigHelper.loadDataVersion(configStorage, TableId.CONSUMER_OFFSET) + .ifPresent(buf -> ConfigHelper.onDataVersionLoad(buf, dataVersion)); + } catch (RocksDBException e) { + LOG.error("Failed to load RocksDB config", e); + return false; + } + return true; + } + + private boolean loadConsumerOffsets() { + // [table-prefix, 1 byte][table-id, 2 bytes][record-prefix, 1 byte] + ByteBuf beginKeyBuf = AbstractRocksDBStorage.POOLED_ALLOCATOR.buffer(4); + beginKeyBuf.writeByte(TablePrefix.TABLE.getValue()); + beginKeyBuf.writeShort(TableId.CONSUMER_OFFSET.getValue()); + beginKeyBuf.writeByte(RecordPrefix.DATA.getValue()); + + ByteBuf endKeyBuf = AbstractRocksDBStorage.POOLED_ALLOCATOR.buffer(4); + endKeyBuf.writeByte(TablePrefix.TABLE.getValue()); + endKeyBuf.writeShort(TableId.CONSUMER_OFFSET.getValue()); + endKeyBuf.writeByte(RecordPrefix.DATA.getValue() + 1); + + try (RocksIterator iterator = configStorage.iterate(beginKeyBuf.nioBuffer(), endKeyBuf.nioBuffer())) { + int keyCapacity = 256; + // We may iterate millions of LMQ consumer offsets here, use direct byte buffers here to avoid memory + // fragment + ByteBuffer keyBuffer = ByteBuffer.allocateDirect(keyCapacity); + ByteBuffer valueBuffer = ByteBuffer.allocateDirect(Long.BYTES); + while (iterator.isValid()) { + keyBuffer.clear(); + valueBuffer.clear(); + + int len = iterator.key(keyBuffer); + if (len > keyCapacity) { + keyCapacity = len; + PlatformDependent.freeDirectBuffer(keyBuffer); + // Reserve more space for key + keyBuffer = ByteBuffer.allocateDirect(keyCapacity); + continue; + } + len = iterator.value(valueBuffer); + assert len == Long.BYTES; + + // skip table-prefix, table-id, record-prefix + keyBuffer.position(1 + 2 + 1); + short groupLen = keyBuffer.getShort(); + byte[] groupBytes = new byte[groupLen]; + keyBuffer.get(groupBytes); + byte ctrl = keyBuffer.get(); + assert ctrl == AbstractRocksDBStorage.CTRL_1; + + short topicLen = keyBuffer.getShort(); + byte[] topicBytes = new byte[topicLen]; + keyBuffer.get(topicBytes); + String topic = new String(topicBytes, StandardCharsets.UTF_8); + ctrl = keyBuffer.get(); + assert ctrl == AbstractRocksDBStorage.CTRL_1; + + int queueId = keyBuffer.getInt(); + + long offset = valueBuffer.getLong(); + + if (!MixAll.isLmq(topic)) { + String group = new String(groupBytes, StandardCharsets.UTF_8); + onConsumerOffsetRecordLoad(topic, group, queueId, offset); + } + iterator.next(); + } + PlatformDependent.freeDirectBuffer(keyBuffer); + PlatformDependent.freeDirectBuffer(valueBuffer); + } finally { + beginKeyBuf.release(); + endKeyBuf.release(); + } + return true; + } + + private void onConsumerOffsetRecordLoad(String topic, String group, int queueId, long offset) { + if (MixAll.isLmq(topic)) { + return; + } + String key = topic + TOPIC_GROUP_SEPARATOR + group; + if (!offsetTable.containsKey(key)) { + ConcurrentMap map = new ConcurrentHashMap<>(); + offsetTable.putIfAbsent(key, map); + } + offsetTable.get(key).put(queueId, offset); + } + + @Override + public long queryOffset(String group, String topic, int queueId) { + if (!MixAll.isLmq(topic)) { + return super.queryOffset(group, topic, queueId); + } + + ByteBuf keyBuf = keyOfConsumerOffset(group, topic, queueId); + try { + byte[] slice = configStorage.get(keyBuf.nioBuffer()); + if (null == slice) { + return -1; + } + assert slice.length == Long.BYTES; + return ByteBuffer.wrap(slice).getLong(); + } catch (RocksDBException e) { + throw new RuntimeException(e); + } finally { + keyBuf.release(); + } + } + + @Override + public void commitPullOffset(String clientHost, String group, String topic, int queueId, long offset) { + if (!MixAll.isLmq(topic)) { + super.commitPullOffset(clientHost, group, topic, queueId, offset); + } + + ByteBuf keyBuf = keyOfPullOffset(group, topic, queueId); + ByteBuf valueBuf = AbstractRocksDBStorage.POOLED_ALLOCATOR.buffer(8); + valueBuf.writeLong(offset); + try (WriteBatch writeBatch = new WriteBatch()) { + writeBatch.put(keyBuf.nioBuffer(), valueBuf.nioBuffer()); + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + ConfigHelper.stampDataVersion(writeBatch, TableId.PULL_OFFSET, dataVersion, stateMachineVersion); + configStorage.write(writeBatch); + } catch (RocksDBException e) { + LOG.error("Failed to commit pull offset. group={}, topic={}, queueId={}, offset={}", + group, topic, queueId, offset); + } finally { + keyBuf.release(); + valueBuf.release(); + } + } + + @Override + public long queryPullOffset(String group, String topic, int queueId) { + if (!MixAll.isLmq(topic)) { + return super.queryPullOffset(group, topic, queueId); + } + + ByteBuf keyBuf = keyOfPullOffset(group, topic, queueId); + try { + byte[] valueBytes = configStorage.get(keyBuf.nioBuffer()); + if (null == valueBytes) { + return -1; + } + return ByteBuffer.wrap(valueBytes).getLong(); + } catch (RocksDBException e) { + LOG.error("Failed to queryPullOffset. group={}, topic={}, queueId={}", group, topic, queueId); + } finally { + keyBuf.release(); + } + return -1; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/config/v2/RecordPrefix.java b/broker/src/main/java/org/apache/rocketmq/broker/config/v2/RecordPrefix.java new file mode 100644 index 0000000..750d454 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/config/v2/RecordPrefix.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.config.v2; + +public enum RecordPrefix { + UNSPECIFIED((byte)0), + DATA_VERSION((byte)1), + DATA((byte)2); + + private final byte value; + + RecordPrefix(byte value) { + this.value = value; + } + + public byte getValue() { + return value; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/config/v2/SerializationType.java b/broker/src/main/java/org/apache/rocketmq/broker/config/v2/SerializationType.java new file mode 100644 index 0000000..2ee157f --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/config/v2/SerializationType.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.config.v2; + +public enum SerializationType { + UNSPECIFIED((byte) 0), + + JSON((byte) 1), + + PROTOBUF((byte) 2), + + FLAT_BUFFERS((byte) 3); + + private final byte value; + + SerializationType(byte value) { + this.value = value; + } + + public byte getValue() { + return value; + } + + public static SerializationType valueOf(byte value) { + for (SerializationType type : SerializationType.values()) { + if (type.getValue() == value) { + return type; + } + } + return SerializationType.UNSPECIFIED; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/config/v2/SubscriptionGroupManagerV2.java b/broker/src/main/java/org/apache/rocketmq/broker/config/v2/SubscriptionGroupManagerV2.java new file mode 100644 index 0000000..dd67871 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/config/v2/SubscriptionGroupManagerV2.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.broker.config.v2; + +import com.alibaba.fastjson2.JSON; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.nio.charset.StandardCharsets; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.subscription.SubscriptionGroupManager; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.config.AbstractRocksDBStorage; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.rocksdb.RocksDBException; +import org.rocksdb.RocksIterator; +import org.rocksdb.WriteBatch; + +public class SubscriptionGroupManagerV2 extends SubscriptionGroupManager { + + private final ConfigStorage configStorage; + + public SubscriptionGroupManagerV2(BrokerController brokerController, ConfigStorage configStorage) { + super(brokerController); + this.configStorage = configStorage; + } + + @Override + public boolean load() { + return loadDataVersion() && loadSubscriptions(); + } + + public boolean loadDataVersion() { + try { + ConfigHelper.loadDataVersion(configStorage, TableId.SUBSCRIPTION_GROUP) + .ifPresent(buf -> { + ConfigHelper.onDataVersionLoad(buf, dataVersion); + }); + } catch (RocksDBException e) { + log.error("loadDataVersion error", e); + return false; + } + return true; + } + + private boolean loadSubscriptions() { + int keyLen = 1 /* table prefix */ + 2 /* table-id */ + 1 /* record-type-prefix */; + ByteBuf beginKey = AbstractRocksDBStorage.POOLED_ALLOCATOR.buffer(keyLen); + beginKey.writeByte(TablePrefix.TABLE.getValue()); + beginKey.writeShort(TableId.SUBSCRIPTION_GROUP.getValue()); + beginKey.writeByte(RecordPrefix.DATA.getValue()); + + ByteBuf endKey = AbstractRocksDBStorage.POOLED_ALLOCATOR.buffer(keyLen); + endKey.writeByte(TablePrefix.TABLE.getValue()); + endKey.writeShort(TableId.SUBSCRIPTION_GROUP.getValue()); + endKey.writeByte(RecordPrefix.DATA.getValue() + 1); + + try (RocksIterator iterator = configStorage.iterate(beginKey.nioBuffer(), endKey.nioBuffer())) { + while (iterator.isValid()) { + SubscriptionGroupConfig subscriptionGroupConfig = parseSubscription(iterator.key(), iterator.value()); + if (null != subscriptionGroupConfig) { + super.putSubscriptionGroupConfig(subscriptionGroupConfig); + } + iterator.next(); + } + } finally { + beginKey.release(); + endKey.release(); + } + return true; + } + + private SubscriptionGroupConfig parseSubscription(byte[] key, byte[] value) { + ByteBuf keyBuf = Unpooled.wrappedBuffer(key); + ByteBuf valueBuf = Unpooled.wrappedBuffer(value); + try { + // Skip table-prefix, table-id, record-type-prefix + keyBuf.readerIndex(4); + short groupNameLen = keyBuf.readShort(); + assert groupNameLen == keyBuf.readableBytes(); + CharSequence groupName = keyBuf.readCharSequence(groupNameLen, StandardCharsets.UTF_8); + assert null != groupName; + byte serializationType = valueBuf.readByte(); + if (SerializationType.JSON == SerializationType.valueOf(serializationType)) { + CharSequence json = valueBuf.readCharSequence(valueBuf.readableBytes(), StandardCharsets.UTF_8); + SubscriptionGroupConfig subscriptionGroupConfig = JSON.parseObject(json.toString(), SubscriptionGroupConfig.class); + assert subscriptionGroupConfig != null; + assert groupName.equals(subscriptionGroupConfig.getGroupName()); + return subscriptionGroupConfig; + } + } finally { + keyBuf.release(); + valueBuf.release(); + } + return null; + } + + @Override + public synchronized void persist() { + try { + configStorage.flushWAL(); + } catch (RocksDBException e) { + log.error("Failed to flush RocksDB WAL", e); + } + } + + @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; + } + ByteBuf keyBuf = ConfigHelper.keyBufOf(TableId.SUBSCRIPTION_GROUP, config.getGroupName()); + ByteBuf valueBuf = ConfigHelper.valueBufOf(config, SerializationType.JSON); + try (WriteBatch writeBatch = new WriteBatch()) { + writeBatch.put(keyBuf.nioBuffer(), valueBuf.nioBuffer()); + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + ConfigHelper.stampDataVersion(writeBatch, TableId.SUBSCRIPTION_GROUP, dataVersion, stateMachineVersion); + configStorage.write(writeBatch); + // fdatasync on core metadata change + persist(); + } catch (RocksDBException e) { + log.error("update subscription group config error", e); + } finally { + keyBuf.release(); + valueBuf.release(); + } + super.updateSubscriptionGroupConfigWithoutPersist(config); + } + + @Override + public boolean containsSubscriptionGroup(String group) { + if (MixAll.isLmq(group)) { + return true; + } else { + return super.containsSubscriptionGroup(group); + } + } + + @Override + protected SubscriptionGroupConfig removeSubscriptionGroupConfig(String groupName) { + ByteBuf keyBuf = ConfigHelper.keyBufOf(TableId.SUBSCRIPTION_GROUP, groupName); + try (WriteBatch writeBatch = new WriteBatch()) { + writeBatch.delete(ConfigHelper.readBytes(keyBuf)); + long stateMachineVersion = brokerController.getMessageStore().getStateMachineVersion(); + ConfigHelper.stampDataVersion(writeBatch, TableId.SUBSCRIPTION_GROUP, dataVersion, stateMachineVersion); + configStorage.write(writeBatch); + } catch (RocksDBException e) { + log.error("Failed to remove subscription group config by group-name={}", groupName, e); + } + return super.removeSubscriptionGroupConfig(groupName); + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/config/v2/TableId.java b/broker/src/main/java/org/apache/rocketmq/broker/config/v2/TableId.java new file mode 100644 index 0000000..7a61899 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/config/v2/TableId.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.broker.config.v2; + +/** + * See Table, Key Value Mapping + */ +public enum TableId { + UNSPECIFIED((short) 0), + CONSUMER_OFFSET((short) 1), + PULL_OFFSET((short) 2), + TOPIC((short) 3), + SUBSCRIPTION_GROUP((short) 4); + + private final short value; + + TableId(short value) { + this.value = value; + } + + public short getValue() { + return value; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/config/v2/TablePrefix.java b/broker/src/main/java/org/apache/rocketmq/broker/config/v2/TablePrefix.java new file mode 100644 index 0000000..d16c14d --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/config/v2/TablePrefix.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.broker.config.v2; + +public enum TablePrefix { + UNSPECIFIED((byte) 0), + TABLE((byte) 1); + + private final byte value; + + TablePrefix(byte value) { + this.value = value; + } + + public byte getValue() { + return value; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/config/v2/TopicConfigManagerV2.java b/broker/src/main/java/org/apache/rocketmq/broker/config/v2/TopicConfigManagerV2.java new file mode 100644 index 0000000..7991d70 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/config/v2/TopicConfigManagerV2.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.broker.config.v2; + +import com.alibaba.fastjson2.JSON; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.nio.charset.StandardCharsets; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.topic.TopicConfigManager; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.config.AbstractRocksDBStorage; +import org.apache.rocketmq.common.constant.PermName; +import org.rocksdb.RocksDBException; +import org.rocksdb.RocksIterator; +import org.rocksdb.WriteBatch; + +/** + * Key layout: [table-prefix, 1 byte][table-id, 2 bytes][record-type-prefix, 1 byte][topic-len, 2 bytes][topic-bytes] + * Value layout: [serialization-type, 1 byte][topic-config-bytes] + */ +public class TopicConfigManagerV2 extends TopicConfigManager { + private final ConfigStorage configStorage; + + public TopicConfigManagerV2(BrokerController brokerController, ConfigStorage configStorage) { + super(brokerController); + this.configStorage = configStorage; + } + + @Override + public boolean load() { + return loadDataVersion() && loadTopicConfig(); + } + + public boolean loadDataVersion() { + try { + ConfigHelper.loadDataVersion(configStorage, TableId.TOPIC) + .ifPresent(buf -> ConfigHelper.onDataVersionLoad(buf, dataVersion)); + } catch (RocksDBException e) { + log.error("Failed to load data version of topic", e); + return false; + } + return true; + } + + private boolean loadTopicConfig() { + int keyLen = 1 /* table-prefix */ + 2 /* table-id */ + 1 /* record-type-prefix */; + ByteBuf beginKey = AbstractRocksDBStorage.POOLED_ALLOCATOR.buffer(keyLen); + beginKey.writeByte(TablePrefix.TABLE.getValue()); + beginKey.writeShort(TableId.TOPIC.getValue()); + beginKey.writeByte(RecordPrefix.DATA.getValue()); + + ByteBuf endKey = AbstractRocksDBStorage.POOLED_ALLOCATOR.buffer(keyLen); + endKey.writeByte(TablePrefix.TABLE.getValue()); + endKey.writeShort(TableId.TOPIC.getValue()); + endKey.writeByte(RecordPrefix.DATA.getValue() + 1); + + try (RocksIterator iterator = configStorage.iterate(beginKey.nioBuffer(), endKey.nioBuffer())) { + while (iterator.isValid()) { + byte[] key = iterator.key(); + byte[] value = iterator.value(); + TopicConfig topicConfig = parseTopicConfig(key, value); + if (null != topicConfig) { + super.putTopicConfig(topicConfig); + } + iterator.next(); + } + } finally { + beginKey.release(); + endKey.release(); + } + return true; + } + + /** + * Key layout: [table-prefix, 1 byte][table-id, 2 bytes][record-type-prefix, 1 byte][topic-len, 2 bytes][topic-bytes] + * Value layout: [serialization-type, 1 byte][topic-config-bytes] + * + * @param key Topic config key representation in RocksDB + * @param value Topic config value representation in RocksDB + * @return decoded topic config + */ + private TopicConfig parseTopicConfig(byte[] key, byte[] value) { + ByteBuf keyBuf = Unpooled.wrappedBuffer(key); + ByteBuf valueBuf = Unpooled.wrappedBuffer(value); + try { + // Skip table-prefix, table-id, record-type-prefix + keyBuf.readerIndex(4); + short topicLen = keyBuf.readShort(); + assert topicLen == keyBuf.readableBytes(); + CharSequence topic = keyBuf.readCharSequence(topicLen, StandardCharsets.UTF_8); + assert null != topic; + + byte serializationType = valueBuf.readByte(); + if (SerializationType.JSON == SerializationType.valueOf(serializationType)) { + CharSequence json = valueBuf.readCharSequence(valueBuf.readableBytes(), StandardCharsets.UTF_8); + TopicConfig topicConfig = JSON.parseObject(json.toString(), TopicConfig.class); + assert topicConfig != null; + assert topic.equals(topicConfig.getTopicName()); + return topicConfig; + } + } finally { + keyBuf.release(); + valueBuf.release(); + } + + return null; + } + + @Override + public synchronized void persist() { + try { + configStorage.flushWAL(); + } catch (RocksDBException e) { + log.error("Failed to flush WAL", e); + } + } + + @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.updateSingleTopicConfigWithoutPersist(topicConfig); + + ByteBuf keyBuf = ConfigHelper.keyBufOf(TableId.TOPIC, topicConfig.getTopicName()); + ByteBuf valueBuf = ConfigHelper.valueBufOf(topicConfig, SerializationType.JSON); + try (WriteBatch writeBatch = new WriteBatch()) { + writeBatch.put(keyBuf.nioBuffer(), valueBuf.nioBuffer()); + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + ConfigHelper.stampDataVersion(writeBatch, TableId.TOPIC, dataVersion, stateMachineVersion); + configStorage.write(writeBatch); + // fdatasync on core metadata change + this.persist(); + } catch (RocksDBException e) { + log.error("Failed to update topic config", e); + } finally { + keyBuf.release(); + valueBuf.release(); + } + } + + @Override + protected TopicConfig removeTopicConfig(String topicName) { + ByteBuf keyBuf = ConfigHelper.keyBufOf(TableId.TOPIC, topicName); + try (WriteBatch writeBatch = new WriteBatch()) { + writeBatch.delete(keyBuf.nioBuffer()); + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + ConfigHelper.stampDataVersion(writeBatch, TableId.TOPIC, dataVersion, stateMachineVersion); + configStorage.write(writeBatch); + } catch (RocksDBException e) { + log.error("Failed to delete topic config by topicName={}", topicName, e); + } finally { + keyBuf.release(); + } + return super.removeTopicConfig(topicName); + } + + @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/config/v2/package-info.java b/broker/src/main/java/org/apache/rocketmq/broker/config/v2/package-info.java new file mode 100644 index 0000000..1ea2161 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/config/v2/package-info.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.config.v2; + +/* + * Endian: we use network byte order for all integrals, aka, always big endian. + * + * Unlike v1 config managers, implementations in this package prioritize data integrity and reliability. + * As a result,RocksDB write-ahead-log is always on and changes are immediately flushed. Another significant + * difference is that heap-based cache is removed because it is not necessary and duplicated to RocksDB + * MemTable/BlockCache. + */ 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 0000000..f22f22a --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/controller/ReplicasManager.java @@ -0,0 +1,882 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.collections.CollectionUtils; +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: {}", tempBrokerMetadata.getBrokerId(), e); + 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 metadata. + */ + 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()) { + List adders = brokerOuterAPI.dnsLookupAddressByDomain(this.brokerConfig.getControllerAddr()); + if (CollectionUtils.isNotEmpty(adders)) { + this.controllerAddresses = adders; + } + } 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 0000000..e6cb976 --- /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 0000000..dd37f42 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/failover/EscapeBridge.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.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.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Triple; +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.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; +import org.apache.rocketmq.tieredstore.TieredMessageStore; + +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, null); + 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); + } + } + + public SendResult putMessageToRemoteBroker(MessageExtBrokerInner messageExt, String brokerNameToSend) { + if (this.brokerController.getBrokerConfig().getBrokerName().equals(brokerNameToSend)) { // not remote broker + return null; + } + 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; + if (StringUtils.isEmpty(brokerNameToSend)) { + mqSelected = topicPublishInfo.selectOneMessageQueue(this.brokerController.getBrokerConfig().getBrokerName()); + messageToPut.setQueueId(mqSelected.getQueueId()); + brokerNameToSend = mqSelected.getBrokerName(); + if (this.brokerController.getBrokerConfig().getBrokerName().equals(brokerNameToSend)) { + LOG.warn("putMessageToRemoteBroker failed, remote broker not found. Topic: {}, MsgId: {}, Broker: {}", + messageExt.getTopic(), messageExt.getMsgId(), brokerNameToSend); + return null; + } + } else { + mqSelected = new MessageQueue(messageExt.getTopic(), brokerNameToSend, messageExt.getQueueId()); + } + + final String brokerAddrToSend = this.brokerController.getTopicRouteInfoManager().findBrokerAddressInPublish(brokerNameToSend); + if (null == brokerAddrToSend) { + LOG.warn("putMessageToRemoteBroker failed, remote broker address not found. Topic: {}, MsgId: {}, Broker: {}", + messageExt.getTopic(), messageExt.getMsgId(), brokerNameToSend); + return null; + } + + 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(this::transformSendResult2PutResult, 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); + } + try { + return asyncRemotePutMessageToSpecificQueue(messageExt).get(SEND_TIMEOUT, TimeUnit.MILLISECONDS); + } catch (Exception e) { + LOG.error("Put message to specific queue error", e); + return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, null, true); + } + } + + public CompletableFuture asyncPutMessageToSpecificQueue(MessageExtBrokerInner messageExt) { + BrokerController masterBroker = this.brokerController.peekMasterBroker(); + if (masterBroker != null) { + return masterBroker.getMessageStore().asyncPutMessage(messageExt); + } + return asyncRemotePutMessageToSpecificQueue(messageExt); + } + + public CompletableFuture asyncRemotePutMessageToSpecificQueue(MessageExtBrokerInner messageExt) { + 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 CompletableFuture.completedFuture(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); + return this.brokerController.getBrokerOuterAPI().sendMessageToSpecificBrokerAsync( + brokerAddrToSend, brokerNameToSend, + messageExt, this.getProducerGroup(messageExt), SEND_TIMEOUT).thenCompose(sendResult -> CompletableFuture.completedFuture(transformSendResult2PutResult(sendResult))); + } 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 to specific queue failed, enableSlaveActingMaster={}, enableRemoteEscape={}.", + this.brokerController.getBrokerConfig().isEnableSlaveActingMaster(), this.brokerController.getBrokerConfig().isEnableRemoteEscape()); + return CompletableFuture.completedFuture(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 Triple getMessage(String topic, long offset, int queueId, String brokerName, + boolean deCompressBody) { + return getMessageAsync(topic, offset, queueId, brokerName, deCompressBody).join(); + } + + // Triple, check info and retry if and only if MessageExt is null + 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 Triple.of(null, "getMessageResult is null", false); // local store, so no retry + } + List list = decodeMsgList(result, deCompressBody); + if (list == null || list.isEmpty()) { + // OFFSET_FOUND_NULL returned by TieredMessageStore indicates exception occurred + boolean needRetry = GetMessageStatus.OFFSET_FOUND_NULL.equals(result.getStatus()) + && messageStore instanceof TieredMessageStore; + LOG.warn("Can not get msg , topic {}, offset {}, queueId {}, needRetry {}, result is {}", + topic, offset, queueId, needRetry, result); + return Triple.of(null, "Can not get msg", needRetry); + } + return Triple.of(list.get(0), "", false); + }); + } else { + return getMessageFromRemoteAsync(topic, offset, queueId, brokerName); + } + } + + 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 Triple getMessageFromRemote(String topic, long offset, int queueId, + String brokerName) { + return getMessageFromRemoteAsync(topic, offset, queueId, brokerName).join(); + } + + // Triple, check info and retry if and only if MessageExt is null + 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, brokerName); + return CompletableFuture.completedFuture(Triple.of(null, "brokerAddress not found", true)); // maybe offline temporarily, so need retry + } + } + + return this.brokerController.getBrokerOuterAPI().pullMessageFromSpecificBrokerAsync(brokerName, + brokerAddr, this.innerConsumerGroupName, topic, queueId, offset, 1, DEFAULT_PULL_TIMEOUT_MILLIS) + .thenApply(pullResult -> { + if (pullResult.getLeft() != null + && PullStatus.FOUND.equals(pullResult.getLeft().getPullStatus()) + && CollectionUtils.isNotEmpty(pullResult.getLeft().getMsgFoundList())) { + return Triple.of(pullResult.getLeft().getMsgFoundList().get(0), "", false); + } + return Triple.of(null, pullResult.getMiddle(), pullResult.getRight()); + }); + } catch (Exception e) { + LOG.error("Get message from remote failed. {}, {}, {}, {}", topic, offset, queueId, brokerName, e); + } + + return CompletableFuture.completedFuture(Triple.of(null, "Get message from remote failed", true)); // need retry + } +} 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 new file mode 100644 index 0000000..00f0c13 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/filter/CommitLogDispatcherCalcBitMap.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.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 java.util.Collection; +import java.util.Iterator; + +/** + * Calculate bit map of filter. + */ +public class CommitLogDispatcherCalcBitMap implements CommitLogDispatcher { + + private static final Logger log = LoggerFactory.getLogger(LoggerName.FILTER_LOGGER_NAME); + + protected final BrokerConfig brokerConfig; + protected final ConsumerFilterManager consumerFilterManager; + + public CommitLogDispatcherCalcBitMap(BrokerConfig brokerConfig, ConsumerFilterManager consumerFilterManager) { + this.brokerConfig = brokerConfig; + this.consumerFilterManager = consumerFilterManager; + } + + @Override + public void dispatch(DispatchRequest request) { + if (!this.brokerConfig.isEnableCalcFilterBitMap()) { + return; + } + + try { + + Collection filterDatas = consumerFilterManager.get(request.getTopic()); + + if (filterDatas == null || filterDatas.isEmpty()) { + return; + } + + Iterator iterator = filterDatas.iterator(); + BitsArray filterBitMap = BitsArray.create( + this.consumerFilterManager.getBloomFilter().getM() + ); + + long startTime = System.currentTimeMillis(); + while (iterator.hasNext()) { + ConsumerFilterData filterData = iterator.next(); + + if (filterData.getCompiledExpression() == null) { + log.error("[BUG] Consumer in filter manager has no compiled expression! {}", filterData); + continue; + } + + if (filterData.getBloomFilterData() == null) { + log.error("[BUG] Consumer in filter manager has no bloom data! {}", filterData); + continue; + } + + Object ret = null; + try { + MessageEvaluationContext context = new MessageEvaluationContext(request.getPropertiesMap()); + + ret = filterData.getCompiledExpression().evaluate(context); + } catch (Throwable e) { + log.error("Calc filter bit map error!commitLogOffset={}, consumer={}, {}", request.getCommitLogOffset(), filterData, e); + } + + log.debug("Result of Calc bit map:ret={}, data={}, props={}, offset={}", ret, filterData, request.getPropertiesMap(), request.getCommitLogOffset()); + + // eval true + if (ret != null && ret instanceof Boolean && (Boolean) ret) { + consumerFilterManager.getBloomFilter().hashTo( + filterData.getBloomFilterData(), + filterBitMap + ); + } + } + + request.setBitMap(filterBitMap.bytes()); + + long elapsedTime = UtilAll.computeElapsedTimeMilliseconds(startTime); + // 1ms + 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/ConsumerFilterData.java b/broker/src/main/java/org/apache/rocketmq/broker/filter/ConsumerFilterData.java new file mode 100644 index 0000000..ee16a61 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/filter/ConsumerFilterData.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.broker.filter; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.apache.rocketmq.filter.expression.Expression; +import org.apache.rocketmq.filter.util.BloomFilterData; + +import java.util.Collections; + +/** + * Filter data of consumer. + */ +public class ConsumerFilterData { + + private String consumerGroup; + private String topic; + private String expression; + private String expressionType; + private transient Expression compiledExpression; + private long bornTime; + private long deadTime = 0; + private BloomFilterData bloomFilterData; + private long clientVersion; + + public boolean isDead() { + return this.deadTime >= this.bornTime; + } + + public long howLongAfterDeath() { + if (isDead()) { + return System.currentTimeMillis() - getDeadTime(); + } + return -1; + } + + /** + * Check this filter data has been used to calculate bit map when msg was stored in server. + */ + public boolean isMsgInLive(long msgStoreTime) { + return msgStoreTime > getBornTime(); + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(final String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public String getTopic() { + return topic; + } + + public void setTopic(final String topic) { + this.topic = topic; + } + + public String getExpression() { + return expression; + } + + public void setExpression(final String expression) { + this.expression = expression; + } + + public String getExpressionType() { + return expressionType; + } + + public void setExpressionType(final String expressionType) { + this.expressionType = expressionType; + } + + public Expression getCompiledExpression() { + return compiledExpression; + } + + public void setCompiledExpression(final Expression compiledExpression) { + this.compiledExpression = compiledExpression; + } + + public long getBornTime() { + return bornTime; + } + + public void setBornTime(final long bornTime) { + this.bornTime = bornTime; + } + + public long getDeadTime() { + return deadTime; + } + + public void setDeadTime(final long deadTime) { + this.deadTime = deadTime; + } + + public BloomFilterData getBloomFilterData() { + return bloomFilterData; + } + + public void setBloomFilterData(final BloomFilterData bloomFilterData) { + this.bloomFilterData = bloomFilterData; + } + + public long getClientVersion() { + return clientVersion; + } + + public void setClientVersion(long clientVersion) { + this.clientVersion = clientVersion; + } + + @Override + public boolean equals(Object o) { + return EqualsBuilder.reflectionEquals(this, o, Collections.emptyList()); + } + + @Override + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this, Collections.emptyList()); + } + + @Override + public String toString() { + return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } +} 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 new file mode 100644 index 0000000..3a48f96 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/filter/ConsumerFilterManager.java @@ -0,0 +1,468 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.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.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; + +/** + * Consumer filter data manager.Just manage the consumers use expression filter. + */ +public class ConsumerFilterManager extends ConfigManager { + + private static final Logger log = LoggerFactory.getLogger(LoggerName.FILTER_LOGGER_NAME); + + private static final long MS_24_HOUR = 24 * 3600 * 1000; + + private ConcurrentMap + filterDataByTopic = new ConcurrentHashMap<>(256); + + private transient BrokerController brokerController; + private transient BloomFilter bloomFilter; + + public ConsumerFilterManager() { + // just for test + this.bloomFilter = BloomFilter.createByFn(20, 64); + } + + public ConsumerFilterManager(BrokerController brokerController) { + this.brokerController = brokerController; + this.bloomFilter = BloomFilter.createByFn( + brokerController.getBrokerConfig().getMaxErrorRateOfBloomFilter(), + brokerController.getBrokerConfig().getExpectConsumerNumUseFilter() + ); + // then set bit map length of store config. + brokerController.getMessageStoreConfig().setBitMapLengthConsumeQueueExt( + this.bloomFilter.getM() + ); + } + + /** + * Build consumer filter data.Be care, bloom filter data is not included. + * + * @return maybe null + */ + public static ConsumerFilterData build(final String topic, final String consumerGroup, + final String expression, final String type, + final long clientVersion) { + if (ExpressionType.isTagType(type)) { + return null; + } + + ConsumerFilterData consumerFilterData = new ConsumerFilterData(); + consumerFilterData.setTopic(topic); + consumerFilterData.setConsumerGroup(consumerGroup); + consumerFilterData.setBornTime(System.currentTimeMillis()); + consumerFilterData.setDeadTime(0); + consumerFilterData.setExpression(expression); + consumerFilterData.setExpressionType(type); + consumerFilterData.setClientVersion(clientVersion); + try { + consumerFilterData.setCompiledExpression( + FilterFactory.INSTANCE.get(type).compile(expression) + ); + } catch (Throwable e) { + log.error("parse error: expr={}, topic={}, group={}, error={}", expression, topic, consumerGroup, e.getMessage()); + return null; + } + + return consumerFilterData; + } + + public void register(final String consumerGroup, final Collection subList) { + for (SubscriptionData subscriptionData : subList) { + register( + subscriptionData.getTopic(), + consumerGroup, + subscriptionData.getSubString(), + subscriptionData.getExpressionType(), + subscriptionData.getSubVersion() + ); + } + + // make illegal topic dead. + Collection groupFilterData = getByGroup(consumerGroup); + + Iterator iterator = groupFilterData.iterator(); + while (iterator.hasNext()) { + ConsumerFilterData filterData = iterator.next(); + + boolean exist = false; + for (SubscriptionData subscriptionData : subList) { + if (subscriptionData.getTopic().equals(filterData.getTopic())) { + exist = true; + break; + } + } + + if (!exist && !filterData.isDead()) { + filterData.setDeadTime(System.currentTimeMillis()); + log.info("Consumer filter changed: {}, make illegal topic dead:{}", consumerGroup, filterData); + } + } + } + + public boolean register(final String topic, final String consumerGroup, final String expression, + final String type, final long clientVersion) { + if (ExpressionType.isTagType(type)) { + return false; + } + + if (expression == null || expression.length() == 0) { + return false; + } + + FilterDataMapByTopic filterDataMapByTopic = this.filterDataByTopic.get(topic); + + if (filterDataMapByTopic == null) { + FilterDataMapByTopic temp = new FilterDataMapByTopic(topic); + FilterDataMapByTopic prev = this.filterDataByTopic.putIfAbsent(topic, temp); + filterDataMapByTopic = prev != null ? prev : temp; + } + + BloomFilterData bloomFilterData = bloomFilter.generate(consumerGroup + "#" + topic); + + return filterDataMapByTopic.register(consumerGroup, expression, type, bloomFilterData, clientVersion); + } + + public void unRegister(final String consumerGroup) { + for (Entry entry : filterDataByTopic.entrySet()) { + entry.getValue().unRegister(consumerGroup); + } + } + + public ConsumerFilterData get(final String topic, final String consumerGroup) { + if (!this.filterDataByTopic.containsKey(topic)) { + return null; + } + if (this.filterDataByTopic.get(topic).getGroupFilterData().isEmpty()) { + return null; + } + + return this.filterDataByTopic.get(topic).getGroupFilterData().get(consumerGroup); + } + + public Collection getByGroup(final String consumerGroup) { + Collection ret = new HashSet<>(); + + Iterator topicIterator = this.filterDataByTopic.values().iterator(); + while (topicIterator.hasNext()) { + FilterDataMapByTopic filterDataMapByTopic = topicIterator.next(); + + Iterator filterDataIterator = filterDataMapByTopic.getGroupFilterData().values().iterator(); + + while (filterDataIterator.hasNext()) { + ConsumerFilterData filterData = filterDataIterator.next(); + + if (filterData.getConsumerGroup().equals(consumerGroup)) { + ret.add(filterData); + } + } + } + + return ret; + } + + public final Collection get(final String topic) { + if (!this.filterDataByTopic.containsKey(topic)) { + return null; + } + if (this.filterDataByTopic.get(topic).getGroupFilterData().isEmpty()) { + return null; + } + + return this.filterDataByTopic.get(topic).getGroupFilterData().values(); + } + + public BloomFilter getBloomFilter() { + return bloomFilter; + } + + @Override + public String encode() { + return encode(false); + } + + @Override + public String configFilePath() { + if (this.brokerController != null) { + return BrokerPathConfigHelper.getConsumerFilterPath( + this.brokerController.getMessageStoreConfig().getStorePathRootDir() + ); + } + return BrokerPathConfigHelper.getConsumerFilterPath("./unit_test"); + } + + @Override + public void decode(final String jsonString) { + ConsumerFilterManager load = RemotingSerializable.fromJson(jsonString, ConsumerFilterManager.class); + if (load != null && load.filterDataByTopic != null) { + boolean bloomChanged = false; + for (Entry entry : load.filterDataByTopic.entrySet()) { + FilterDataMapByTopic dataMapByTopic = entry.getValue(); + if (dataMapByTopic == null) { + continue; + } + + for (Entry groupEntry : dataMapByTopic.getGroupFilterData().entrySet()) { + + ConsumerFilterData filterData = groupEntry.getValue(); + + if (filterData == null) { + continue; + } + + try { + filterData.setCompiledExpression( + FilterFactory.INSTANCE.get(filterData.getExpressionType()).compile(filterData.getExpression()) + ); + } catch (Exception e) { + log.error("load filter data error, " + filterData, e); + } + + // check whether bloom filter is changed + // if changed, ignore the bit map calculated before. + if (!this.bloomFilter.isValid(filterData.getBloomFilterData())) { + bloomChanged = true; + log.info("Bloom filter is changed!So ignore all filter data persisted! {}, {}", this.bloomFilter, filterData.getBloomFilterData()); + break; + } + + log.info("load exist consumer filter data: {}", filterData); + + if (filterData.getDeadTime() == 0) { + // we think all consumers are dead when load + long deadTime = System.currentTimeMillis() - 30 * 1000; + filterData.setDeadTime( + deadTime <= filterData.getBornTime() ? filterData.getBornTime() : deadTime + ); + } + } + } + + if (!bloomChanged) { + this.filterDataByTopic = load.filterDataByTopic; + } + } + } + + @Override + public String encode(final boolean prettyFormat) { + // clean + { + clean(); + } + return RemotingSerializable.toJson(this, prettyFormat); + } + + public void clean() { + Iterator> topicIterator = this.filterDataByTopic.entrySet().iterator(); + while (topicIterator.hasNext()) { + Map.Entry filterDataMapByTopic = topicIterator.next(); + + Iterator> filterDataIterator + = filterDataMapByTopic.getValue().getGroupFilterData().entrySet().iterator(); + + while (filterDataIterator.hasNext()) { + Map.Entry filterDataByGroup = filterDataIterator.next(); + + ConsumerFilterData filterData = filterDataByGroup.getValue(); + if (filterData.howLongAfterDeath() >= (this.brokerController == null ? MS_24_HOUR : this.brokerController.getBrokerConfig().getFilterDataCleanTimeSpan())) { + log.info("Remove filter consumer {}, died too long!", filterDataByGroup.getValue()); + filterDataIterator.remove(); + } + } + + if (filterDataMapByTopic.getValue().getGroupFilterData().isEmpty()) { + log.info("Topic has no consumer, remove it! {}", filterDataMapByTopic.getKey()); + topicIterator.remove(); + } + } + } + + public ConcurrentMap getFilterDataByTopic() { + return filterDataByTopic; + } + + public void setFilterDataByTopic(final ConcurrentHashMap filterDataByTopic) { + this.filterDataByTopic = filterDataByTopic; + } + + public static class FilterDataMapByTopic { + + private ConcurrentMap + groupFilterData = new ConcurrentHashMap<>(); + + private String topic; + + public FilterDataMapByTopic() { + } + + public FilterDataMapByTopic(String topic) { + this.topic = topic; + } + + public void unRegister(String consumerGroup) { + if (!this.groupFilterData.containsKey(consumerGroup)) { + return; + } + + ConsumerFilterData data = this.groupFilterData.get(consumerGroup); + + if (data == null || data.isDead()) { + return; + } + + long now = System.currentTimeMillis(); + + log.info("Unregister consumer filter: {}, deadTime: {}", data, now); + + data.setDeadTime(now); + } + + public boolean register(String consumerGroup, String expression, String type, BloomFilterData bloomFilterData, + long clientVersion) { + ConsumerFilterData old = this.groupFilterData.get(consumerGroup); + + if (old == null) { + ConsumerFilterData consumerFilterData = build(topic, consumerGroup, expression, type, clientVersion); + if (consumerFilterData == null) { + return false; + } + consumerFilterData.setBloomFilterData(bloomFilterData); + + old = this.groupFilterData.putIfAbsent(consumerGroup, consumerFilterData); + if (old == null) { + log.info("New consumer filter registered: {}", consumerFilterData); + return true; + } else { + if (clientVersion <= old.getClientVersion()) { + if (!type.equals(old.getExpressionType()) || !expression.equals(old.getExpression())) { + log.warn("Ignore consumer({} : {}) filter(concurrent), because of version {} <= {}, but maybe info changed!old={}:{}, ignored={}:{}", + consumerGroup, topic, + clientVersion, old.getClientVersion(), + old.getExpressionType(), old.getExpression(), + type, expression); + } + if (clientVersion == old.getClientVersion() && old.isDead()) { + reAlive(old); + return true; + } + + return false; + } else { + this.groupFilterData.put(consumerGroup, consumerFilterData); + log.info("New consumer filter registered(concurrent): {}, old: {}", consumerFilterData, old); + return true; + } + } + } else { + if (clientVersion <= old.getClientVersion()) { + if (!type.equals(old.getExpressionType()) || !expression.equals(old.getExpression())) { + log.info("Ignore consumer({}:{}) filter, because of version {} <= {}, but maybe info changed!old={}:{}, ignored={}:{}", + consumerGroup, topic, + clientVersion, old.getClientVersion(), + old.getExpressionType(), old.getExpression(), + type, expression); + } + if (clientVersion == old.getClientVersion() && old.isDead()) { + reAlive(old); + return true; + } + + return false; + } + + boolean change = !old.getExpression().equals(expression) || !old.getExpressionType().equals(type); + if (old.getBloomFilterData() == null && bloomFilterData != null) { + change = true; + } + if (old.getBloomFilterData() != null && !old.getBloomFilterData().equals(bloomFilterData)) { + change = true; + } + + // if subscribe data is changed, or consumer is died too long. + if (change) { + ConsumerFilterData consumerFilterData = build(topic, consumerGroup, expression, type, clientVersion); + if (consumerFilterData == null) { + // new expression compile error, remove old, let client report error. + this.groupFilterData.remove(consumerGroup); + return false; + } + consumerFilterData.setBloomFilterData(bloomFilterData); + + this.groupFilterData.put(consumerGroup, consumerFilterData); + + log.info("Consumer filter info change, old: {}, new: {}, change: {}", + old, consumerFilterData, change); + + return true; + } else { + old.setClientVersion(clientVersion); + if (old.isDead()) { + reAlive(old); + } + return true; + } + } + } + + protected void reAlive(ConsumerFilterData filterData) { + long oldDeadTime = filterData.getDeadTime(); + filterData.setDeadTime(0); + log.info("Re alive consumer filter: {}, oldDeadTime: {}", filterData, oldDeadTime); + } + + public final ConsumerFilterData get(String consumerGroup) { + return this.groupFilterData.get(consumerGroup); + } + + public final ConcurrentMap getGroupFilterData() { + return this.groupFilterData; + } + + public void setGroupFilterData(final ConcurrentHashMap groupFilterData) { + this.groupFilterData = groupFilterData; + } + + public String getTopic() { + return topic; + } + + public void setTopic(final String topic) { + this.topic = 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 new file mode 100644 index 0000000..cc3e37b --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/filter/ExpressionForRetryMessageFilter.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.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.remoting.protocol.heartbeat.SubscriptionData; + +/** + * Support filter to retry topic. + *
It will decode properties first in order to get real topic. + */ +public class ExpressionForRetryMessageFilter extends ExpressionMessageFilter { + public ExpressionForRetryMessageFilter(SubscriptionData subscriptionData, ConsumerFilterData consumerFilterData, + ConsumerFilterManager consumerFilterManager) { + super(subscriptionData, consumerFilterData, consumerFilterManager); + } + + @Override + public boolean isMatchedByCommitLog(ByteBuffer msgBuffer, Map properties) { + if (subscriptionData == null) { + return true; + } + + if (subscriptionData.isClassFilterMode()) { + return true; + } + + 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; + if (isRetryTopic) { + // retry topic, use original filter data. + // poor performance to support retry filter. + if (tempProperties == null && msgBuffer != null) { + decoded = true; + tempProperties = MessageDecoder.decodeProperties(msgBuffer); + } + String realTopic = tempProperties.get(MessageConst.PROPERTY_RETRY_TOPIC); + String group = KeyBuilder.parseGroup(subscriptionData.getTopic()); + realFilterData = this.consumerFilterManager.get(realTopic, group); + } + + // no expression + if (realFilterData == null || realFilterData.getExpression() == null + || realFilterData.getCompiledExpression() == null) { + return true; + } + + if (!decoded && tempProperties == null && msgBuffer != null) { + tempProperties = MessageDecoder.decodeProperties(msgBuffer); + } + + Object ret = null; + try { + MessageEvaluationContext context = new MessageEvaluationContext(tempProperties); + + ret = realFilterData.getCompiledExpression().evaluate(context); + } catch (Throwable e) { + log.error("Message Filter error, " + realFilterData + ", " + tempProperties, e); + } + + log.debug("Pull eval result: {}, {}, {}", ret, realFilterData, tempProperties); + + if (ret == null || !(ret instanceof Boolean)) { + return false; + } + + return (Boolean) ret; + } +} 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 new file mode 100644 index 0000000..5d0340c --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/filter/ExpressionMessageFilter.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.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.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; + +public class ExpressionMessageFilter implements MessageFilter { + + protected static final Logger log = LoggerFactory.getLogger(LoggerName.FILTER_LOGGER_NAME); + + protected final SubscriptionData subscriptionData; + protected final ConsumerFilterData consumerFilterData; + protected final ConsumerFilterManager consumerFilterManager; + protected final boolean bloomDataValid; + + public ExpressionMessageFilter(SubscriptionData subscriptionData, ConsumerFilterData consumerFilterData, + ConsumerFilterManager consumerFilterManager) { + this.subscriptionData = subscriptionData; + this.consumerFilterData = consumerFilterData; + this.consumerFilterManager = consumerFilterManager; + if (consumerFilterData == null) { + bloomDataValid = false; + return; + } + BloomFilter bloomFilter = this.consumerFilterManager.getBloomFilter(); + if (bloomFilter != null && bloomFilter.isValid(consumerFilterData.getBloomFilterData())) { + bloomDataValid = true; + } else { + bloomDataValid = false; + } + } + + @Override + public boolean isMatchedByConsumeQueue(Long tagsCode, ConsumeQueueExt.CqExtUnit cqExtUnit) { + if (null == subscriptionData) { + return true; + } + + if (subscriptionData.isClassFilterMode()) { + return true; + } + + // by tags code. + if (ExpressionType.isTagType(subscriptionData.getExpressionType())) { + + if (tagsCode == null) { + return true; + } + + if (subscriptionData.getSubString().equals(SubscriptionData.SUB_ALL)) { + return true; + } + + return subscriptionData.getCodeSet().contains(tagsCode.intValue()); + } else { + // no expression or no bloom + if (consumerFilterData == null || consumerFilterData.getExpression() == null + || consumerFilterData.getCompiledExpression() == null || consumerFilterData.getBloomFilterData() == null) { + return true; + } + + // message is before consumer + if (cqExtUnit == null || !consumerFilterData.isMsgInLive(cqExtUnit.getMsgStoreTime())) { + log.debug("Pull matched because not in live: {}, {}", consumerFilterData, cqExtUnit); + return true; + } + + byte[] filterBitMap = cqExtUnit.getFilterBitMap(); + BloomFilter bloomFilter = this.consumerFilterManager.getBloomFilter(); + if (filterBitMap == null || !this.bloomDataValid + || filterBitMap.length * Byte.SIZE != consumerFilterData.getBloomFilterData().getBitNum()) { + return true; + } + + BitsArray bitsArray = null; + try { + bitsArray = BitsArray.create(filterBitMap); + boolean ret = bloomFilter.isHit(consumerFilterData.getBloomFilterData(), bitsArray); + log.debug("Pull {} by bit map:{}, {}, {}", ret, consumerFilterData, bitsArray, cqExtUnit); + return ret; + } catch (Throwable e) { + log.error("bloom filter error, sub=" + subscriptionData + + ", filter=" + consumerFilterData + ", bitMap=" + bitsArray, e); + } + } + + return true; + } + + @Override + public boolean isMatchedByCommitLog(ByteBuffer msgBuffer, Map properties) { + if (subscriptionData == null) { + return true; + } + + if (subscriptionData.isClassFilterMode()) { + return true; + } + + if (ExpressionType.isTagType(subscriptionData.getExpressionType())) { + return true; + } + + ConsumerFilterData realFilterData = this.consumerFilterData; + Map tempProperties = properties; + + // no expression + if (realFilterData == null || realFilterData.getExpression() == null + || realFilterData.getCompiledExpression() == null) { + return true; + } + + if (tempProperties == null && msgBuffer != null) { + tempProperties = MessageDecoder.decodeProperties(msgBuffer); + } + + Object ret = null; + try { + MessageEvaluationContext context = new MessageEvaluationContext(tempProperties); + + ret = realFilterData.getCompiledExpression().evaluate(context); + } catch (Throwable e) { + log.error("Message Filter error, " + realFilterData + ", " + tempProperties, e); + } + + log.debug("Pull eval result: {}, {}, {}", ret, realFilterData, tempProperties); + + if (ret == null || !(ret instanceof Boolean)) { + return false; + } + + return (Boolean) ret; + } + +} 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 new file mode 100644 index 0000000..980b738 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/filter/MessageEvaluationContext.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.broker.filter; + +import org.apache.rocketmq.filter.expression.EvaluationContext; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Evaluation context from message. + */ +public class MessageEvaluationContext implements EvaluationContext { + + private Map properties; + + public MessageEvaluationContext(Map properties) { + this.properties = properties; + } + + @Override + public Object get(final String name) { + if (this.properties == null) { + return null; + } + return this.properties.get(name); + } + + @Override + public Map keyValues() { + if (properties == null) { + return null; + } + + Map copy = new HashMap<>(properties.size(), 1); + + for (Entry entry : properties.entrySet()) { + copy.put(entry.getKey(), entry.getValue()); + } + + return copy; + } +} 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 new file mode 100644 index 0000000..ce8fdd8 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/latency/BrokerFastFailure.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.broker.latency; + +import java.util.List; +import java.util.ArrayList; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.AbstractBrokerRunnable; +import org.apache.rocketmq.common.Pair; +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; + +/** + * BrokerFastFailure will cover {@link BrokerController#getSendThreadPoolQueue()} and {@link + * BrokerController#getPullThreadPoolQueue()} + */ +public class BrokerFastFailure { + 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(); + + private final List, Supplier>> cleanExpiredRequestQueueList = new ArrayList<>(); + + public BrokerFastFailure(final BrokerController brokerController) { + this.brokerController = brokerController; + initCleanExpiredRequestQueueList(); + this.scheduledExecutorService = ThreadUtils.newScheduledThreadPool(1, + new ThreadFactoryImpl("BrokerFastFailureScheduledThread", true, + brokerController == null ? null : brokerController.getBrokerConfig())); + } + + private void initCleanExpiredRequestQueueList() { + cleanExpiredRequestQueueList.add(new Pair<>(this.brokerController.getSendThreadPoolQueue(), () -> this.brokerController.getBrokerConfig().getWaitTimeMillsInSendQueue())); + cleanExpiredRequestQueueList.add(new Pair<>(this.brokerController.getPullThreadPoolQueue(), () -> this.brokerController.getBrokerConfig().getWaitTimeMillsInPullQueue())); + cleanExpiredRequestQueueList.add(new Pair<>(this.brokerController.getLitePullThreadPoolQueue(), () -> this.brokerController.getBrokerConfig().getWaitTimeMillsInLitePullQueue())); + cleanExpiredRequestQueueList.add(new Pair<>(this.brokerController.getHeartbeatThreadPoolQueue(), () -> this.brokerController.getBrokerConfig().getWaitTimeMillsInHeartbeatQueue())); + cleanExpiredRequestQueueList.add(new Pair<>(this.brokerController.getEndTransactionThreadPoolQueue(), () -> this.brokerController.getBrokerConfig().getWaitTimeMillsInTransactionQueue())); + cleanExpiredRequestQueueList.add(new Pair<>(this.brokerController.getAckThreadPoolQueue(), () -> this.brokerController.getBrokerConfig().getWaitTimeMillsInAckQueue())); + cleanExpiredRequestQueueList.add(new Pair<>(this.brokerController.getAdminBrokerThreadPoolQueue(), () -> this.brokerController.getBrokerConfig().getWaitTimeMillsInAdminBrokerQueue())); + } + + public static RequestTask castRunnable(final Runnable runnable) { + try { + if (runnable instanceof FutureTaskExt) { + FutureTaskExt object = (FutureTaskExt) runnable; + return (RequestTask) object.getRunnable(); + } + } catch (Throwable e) { + LOGGER.error(String.format("castRunnable exception, %s", runnable.getClass().getName()), e); + } + + return null; + } + + public void start() { + this.scheduledExecutorService.scheduleAtFixedRate(new AbstractBrokerRunnable(this.brokerController.getBrokerConfig()) { + @Override + public void run0() { + if (brokerController.getBrokerConfig().isBrokerFastFailureEnable()) { + cleanExpiredRequest(); + } + } + }, 1000, 10, TimeUnit.MILLISECONDS); + } + + private void cleanExpiredRequest() { + + while (this.brokerController.getMessageStore().isOSPageCacheBusy()) { + try { + if (!this.brokerController.getSendThreadPoolQueue().isEmpty()) { + final Runnable runnable = this.brokerController.getSendThreadPoolQueue().poll(0, TimeUnit.SECONDS); + if (null == runnable) { + break; + } + + final RequestTask rt = castRunnable(runnable); + 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; + } + } catch (Throwable ignored) { + } + } + + for (Pair, Supplier> pair : cleanExpiredRequestQueueList) { + cleanExpiredRequestInQueue(pair.getObject1(), pair.getObject2().get()); + } + } + + void cleanExpiredRequestInQueue(final BlockingQueue blockingQueue, final long maxWaitTimeMillsInQueue) { + while (true) { + try { + 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(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; + } + } else { + break; + } + } catch (Throwable ignored) { + } + } + } + + public synchronized void addCleanExpiredRequestQueue(BlockingQueue cleanExpiredRequestQueue, + Supplier maxWaitTimeMillsInQueueSupplier) { + cleanExpiredRequestQueueList.add(new Pair<>(cleanExpiredRequestQueue, maxWaitTimeMillsInQueueSupplier)); + } + + public void shutdown() { + this.scheduledExecutorService.shutdown(); + } +} 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 0000000..0c69e2d --- /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 0000000..eddaee7 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/LmqPullRequestHoldService.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.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)); + try { + final long offset = brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId); + 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 new file mode 100644 index 0000000..08703de --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/ManyPullRequest.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.longpolling; + +import java.util.ArrayList; +import java.util.List; + +public class ManyPullRequest { + private final ArrayList pullRequestList = new ArrayList<>(); + + public synchronized void addPullRequest(final PullRequest pullRequest) { + this.pullRequestList.add(pullRequest); + } + + public synchronized void addPullRequest(final List many) { + this.pullRequestList.addAll(many); + } + + public synchronized List cloneListAndClear() { + if (!this.pullRequestList.isEmpty()) { + List result = (ArrayList) this.pullRequestList.clone(); + this.pullRequestList.clear(); + return result; + } + + 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 0000000..2ff9a73 --- /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 new file mode 100644 index 0000000..9c0ee89 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/NotifyMessageArrivingListener.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.broker.longpolling; + +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, 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) { + + this.pullRequestHoldService.notifyMessageArriving( + topic, queueId, logicOffset, tagsCode, msgStoreTime, filterBitMap, properties); + this.popMessageProcessor.notifyMessageArriving( + topic, queueId, logicOffset, tagsCode, msgStoreTime, filterBitMap, properties); + this.notificationProcessor.notifyMessageArriving( + topic, queueId, logicOffset, tagsCode, msgStoreTime, filterBitMap, properties); + } +} 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 0000000..9f6774a --- /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 0000000..6b7c4fa --- /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/PopCommandCallback.java b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PopCommandCallback.java new file mode 100644 index 0000000..2e190e2 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PopCommandCallback.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.broker.longpolling; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import org.apache.rocketmq.broker.metrics.ConsumerLagCalculator; +import org.apache.rocketmq.remoting.CommandCallback; + +public class PopCommandCallback implements CommandCallback { + + private final BiConsumer> biConsumer; + + private final ConsumerLagCalculator.ProcessGroupInfo info; + private final Consumer lagRecorder; + + + public PopCommandCallback( + BiConsumer> biConsumer, + ConsumerLagCalculator.ProcessGroupInfo info, + Consumer lagRecorder) { + + this.biConsumer = biConsumer; + this.info = info; + this.lagRecorder = lagRecorder; + } + + @Override + public void accept() { + biConsumer.accept(info, lagRecorder); + } +} 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 0000000..e87a8e8 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PopLongPollingService.java @@ -0,0 +1,418 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.ArrayList; +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.CommandCallback; +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 org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.store.ConsumeQueueExt; +import org.apache.rocketmq.store.MessageFilter; + +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 ConcurrentLinkedHashMap> topicCidMap; + private final ConcurrentLinkedHashMap> pollingMap; + private long lastCleanTime = 0; + + private final AtomicLong totalPollingNum = new AtomicLong(0); + private final boolean notifyLast; + + public PopLongPollingService(BrokerController brokerController, NettyRequestProcessor processor, boolean notifyLast) { + this.brokerController = brokerController; + this.processor = processor; + // 100000 topic default, 100000 lru topic + cid + qid + this.topicCidMap = new ConcurrentLinkedHashMap.Builder>() + .maximumWeightedCapacity(this.brokerController.getBrokerConfig().getPopPollingMapSize() * 2L).build(); + this.pollingMap = new ConcurrentLinkedHashMap.Builder>() + .maximumWeightedCapacity(this.brokerController.getBrokerConfig().getPopPollingMapSize()).build(); + this.notifyLast = notifyLast; + } + + @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) { + this.notifyMessageArrivingWithRetryTopic(topic, queueId, -1L, null, 0L, null, null); + } + + public void notifyMessageArrivingWithRetryTopic(final String topic, final int queueId, long offset, + Long tagsCode, long msgStoreTime, byte[] filterBitMap, Map properties) { + String notifyTopic; + if (KeyBuilder.isPopRetryTopicV2(topic)) { + notifyTopic = KeyBuilder.parseNormalTopic(topic); + } else { + notifyTopic = topic; + } + notifyMessageArriving(notifyTopic, queueId, offset, tagsCode, msgStoreTime, filterBitMap, properties); + } + + public void notifyMessageArriving(final String topic, final int queueId, long offset, + Long tagsCode, long msgStoreTime, byte[] filterBitMap, Map properties) { + ConcurrentHashMap cids = topicCidMap.get(topic); + if (cids == null) { + return; + } + long interval = brokerController.getBrokerConfig().getPopLongPollingForceNotifyInterval(); + boolean force = interval > 0L && offset % interval == 0L; + for (Map.Entry cid : cids.entrySet()) { + if (queueId >= 0) { + notifyMessageArriving(topic, -1, cid.getKey(), force, tagsCode, msgStoreTime, filterBitMap, properties); + } + notifyMessageArriving(topic, queueId, cid.getKey(), force, tagsCode, msgStoreTime, filterBitMap, properties); + } + } + + public boolean notifyMessageArriving(final String topic, final int queueId, final String cid, + Long tagsCode, long msgStoreTime, byte[] filterBitMap, Map properties) { + return notifyMessageArriving(topic, queueId, cid, false, tagsCode, msgStoreTime, filterBitMap, properties, null); + } + + public boolean notifyMessageArriving(final String topic, final int queueId, final String cid, boolean force, + Long tagsCode, long msgStoreTime, byte[] filterBitMap, Map properties) { + return notifyMessageArriving(topic, queueId, cid, force, tagsCode, msgStoreTime, filterBitMap, properties, null); + } + + public boolean notifyMessageArriving(final String topic, final int queueId, final String cid, boolean force, + Long tagsCode, long msgStoreTime, byte[] filterBitMap, Map properties, CommandCallback callback) { + ConcurrentSkipListSet remotingCommands = pollingMap.get(KeyBuilder.buildPollingKey(topic, cid, queueId)); + if (remotingCommands == null || remotingCommands.isEmpty()) { + return false; + } + + PopRequest popRequest = pollRemotingCommands(remotingCommands); + if (popRequest == null) { + return false; + } + + if (!force && popRequest.getMessageFilter() != null && popRequest.getSubscriptionData() != null) { + boolean match = popRequest.getMessageFilter().isMatchedByConsumeQueue(tagsCode, + new ConsumeQueueExt.CqExtUnit(tagsCode, msgStoreTime, filterBitMap)); + if (match && properties != null) { + match = popRequest.getMessageFilter().isMatchedByCommitLog(null, properties); + } + if (!match) { + remotingCommands.add(popRequest); + totalPollingNum.incrementAndGet(); + return false; + } + } + + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("lock release, new msg arrive, wakeUp: {}", popRequest); + } + + return wakeUp(popRequest, callback); + } + + public boolean wakeUp(final PopRequest request) { + return wakeUp(request, null); + } + + public boolean wakeUp(final PopRequest request, CommandCallback callback) { + if (request == null || !request.complete()) { + return false; + } + + if (callback != null && request.getRemotingCommand() != null) { + if (request.getRemotingCommand().getCallbackList() == null) { + request.getRemotingCommand().setCallbackList(new ArrayList<>()); + } + request.getRemotingCommand().getCallbackList().add(callback); + } + + 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) { + return this.polling(ctx, remotingCommand, requestHeader, null, null); + } + + public PollingResult polling(final ChannelHandlerContext ctx, RemotingCommand remotingCommand, + final PollingHeader requestHeader, SubscriptionData subscriptionData, MessageFilter messageFilter) { + 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, subscriptionData, messageFilter); + 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 nonexistent 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().containsSubscriptionGroup(cid)) { + POP_LOGGER.info("remove nonexistent subscription group {} 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 nonexistent topic {} in pollingMap!", topic); + pollingMapIter.remove(); + continue; + } + if (!brokerController.getSubscriptionGroupManager().containsSubscriptionGroup(cid)) { + POP_LOGGER.info("remove nonexistent subscription group {} of topic {} in pollingMap!", cid, topic); + pollingMapIter.remove(); + } + } + } + } catch (Throwable e) { + POP_LOGGER.error("cleanUnusedResource", e); + } + + lastCleanTime = System.currentTimeMillis(); + } + + private PopRequest pollRemotingCommands(ConcurrentSkipListSet remotingCommands) { + if (remotingCommands == null || remotingCommands.isEmpty()) { + return null; + } + + PopRequest popRequest; + do { + if (notifyLast) { + popRequest = remotingCommands.pollLast(); + } else { + popRequest = remotingCommands.pollFirst(); + } + totalPollingNum.decrementAndGet(); + } while (popRequest != null && !popRequest.getChannel().isActive()); + + return popRequest; + } +} 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 0000000..0419dbf --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PopRequest.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.broker.longpolling; + +import io.netty.channel.Channel; +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 org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.store.MessageFilter; + +public class PopRequest { + private static final AtomicLong COUNTER = new AtomicLong(Long.MIN_VALUE); + + private final RemotingCommand remotingCommand; + private final ChannelHandlerContext ctx; + private final AtomicBoolean complete = new AtomicBoolean(false); + private final long op = COUNTER.getAndIncrement(); + + private final long expired; + private final SubscriptionData subscriptionData; + private final MessageFilter messageFilter; + + public PopRequest(RemotingCommand remotingCommand, ChannelHandlerContext ctx, + long expired, SubscriptionData subscriptionData, MessageFilter messageFilter) { + + this.ctx = ctx; + this.remotingCommand = remotingCommand; + this.expired = expired; + this.subscriptionData = subscriptionData; + this.messageFilter = messageFilter; + } + + 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; + } + + public SubscriptionData getSubscriptionData() { + return subscriptionData; + } + + public MessageFilter getMessageFilter() { + return messageFilter; + } + + @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 new file mode 100644 index 0000000..5e47105 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PullRequest.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.broker.longpolling; + +import io.netty.channel.Channel; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.store.MessageFilter; + +public class PullRequest { + private final RemotingCommand requestCommand; + private final Channel clientChannel; + private final long timeoutMillis; + private final long suspendTimestamp; + private final long pullFromThisOffset; + private final SubscriptionData subscriptionData; + private final MessageFilter messageFilter; + + public PullRequest(RemotingCommand requestCommand, Channel clientChannel, long timeoutMillis, long suspendTimestamp, + long pullFromThisOffset, SubscriptionData subscriptionData, + MessageFilter messageFilter) { + this.requestCommand = requestCommand; + this.clientChannel = clientChannel; + this.timeoutMillis = timeoutMillis; + this.suspendTimestamp = suspendTimestamp; + this.pullFromThisOffset = pullFromThisOffset; + this.subscriptionData = subscriptionData; + this.messageFilter = messageFilter; + } + + public RemotingCommand getRequestCommand() { + return requestCommand; + } + + public Channel getClientChannel() { + return clientChannel; + } + + public long getTimeoutMillis() { + return timeoutMillis; + } + + public long getSuspendTimestamp() { + return suspendTimestamp; + } + + public long getPullFromThisOffset() { + return pullFromThisOffset; + } + + public SubscriptionData getSubscriptionData() { + return subscriptionData; + } + + public MessageFilter getMessageFilter() { + return messageFilter; + } +} 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 new file mode 100644 index 0000000..7dbc9e4 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PullRequestHoldService.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.broker.longpolling; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.apache.rocketmq.broker.BrokerController; +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.apache.rocketmq.store.exception.ConsumeQueueException; + +public class PullRequestHoldService extends ServiceThread { + private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + protected static final String TOPIC_QUEUEID_SEPARATOR = "@"; + protected final BrokerController brokerController; + private final SystemClock systemClock = new SystemClock(); + protected ConcurrentMap pullRequestTable = + new ConcurrentHashMap<>(1024); + + public PullRequestHoldService(final BrokerController brokerController) { + this.brokerController = brokerController; + } + + public void suspendPullRequest(final String topic, final int queueId, final PullRequest pullRequest) { + String key = this.buildKey(topic, queueId); + ManyPullRequest mpr = this.pullRequestTable.get(key); + if (null == mpr) { + mpr = new ManyPullRequest(); + ManyPullRequest prev = this.pullRequestTable.putIfAbsent(key, mpr); + if (prev != null) { + mpr = prev; + } + } + + pullRequest.getRequestCommand().setSuspended(true); + mpr.addPullRequest(pullRequest); + } + + private String buildKey(final String topic, final int queueId) { + StringBuilder sb = new StringBuilder(topic.length() + 5); + sb.append(topic); + sb.append(TOPIC_QUEUEID_SEPARATOR); + sb.append(queueId); + return sb.toString(); + } + + @Override + public void run() { + log.info("{} service started", this.getServiceName()); + while (!this.isStopped()) { + try { + if (this.brokerController.getBrokerConfig().isLongPollingEnable()) { + this.waitForRunning(5 * 1000); + } else { + this.waitForRunning(this.brokerController.getBrokerConfig().getShortPollingTimeMills()); + } + + long beginLockTimestamp = this.systemClock.now(); + this.checkHoldRequest(); + long costTime = this.systemClock.now() - beginLockTimestamp; + if (costTime > 5 * 1000) { + log.warn("PullRequestHoldService: check hold pull request cost {}ms", costTime); + } + } catch (Throwable e) { + log.warn(this.getServiceName() + " service has exception. ", e); + } + } + + log.info("{} service end", this.getServiceName()); + } + + @Override + public String getServiceName() { + if (brokerController != null && brokerController.getBrokerConfig().isInBrokerContainer()) { + return this.brokerController.getBrokerIdentity().getIdentifier() + PullRequestHoldService.class.getSimpleName(); + } + return PullRequestHoldService.class.getSimpleName(); + } + + protected void checkHoldRequest() { + for (String key : this.pullRequestTable.keySet()) { + String[] kArray = key.split(TOPIC_QUEUEID_SEPARATOR); + if (2 == kArray.length) { + String topic = kArray[0]; + int queueId = Integer.parseInt(kArray[1]); + try { + final long offset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId); + this.notifyMessageArriving(topic, queueId, offset); + } catch (Throwable e) { + log.error( + "PullRequestHoldService: failed to check hold request failed, topic={}, queueId={}", topic, + queueId, e); + } + } + } + } + + public void notifyMessageArriving(final String topic, final int queueId, final long maxOffset) { + notifyMessageArriving(topic, queueId, maxOffset, null, 0, null, null); + } + + public void notifyMessageArriving(final String topic, final int queueId, final long maxOffset, final Long tagsCode, + long msgStoreTime, byte[] filterBitMap, Map properties) { + String key = this.buildKey(topic, queueId); + ManyPullRequest mpr = this.pullRequestTable.get(key); + if (mpr != null) { + List requestList = mpr.cloneListAndClear(); + if (requestList != null) { + List replayList = new ArrayList<>(); + + for (PullRequest request : requestList) { + long newestOffset = maxOffset; + if (newestOffset <= request.getPullFromThisOffset()) { + try { + newestOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId); + } catch (ConsumeQueueException e) { + log.error("Failed tp get max offset in queue", e); + continue; + } + } + + if (newestOffset > request.getPullFromThisOffset()) { + boolean match = request.getMessageFilter().isMatchedByConsumeQueue(tagsCode, + new ConsumeQueueExt.CqExtUnit(tagsCode, msgStoreTime, filterBitMap)); + // match by bit map, need eval again when properties is not null. + if (match && properties != null) { + match = request.getMessageFilter().isMatchedByCommitLog(null, properties); + } + + if (match) { + try { + this.brokerController.getPullMessageProcessor().executeRequestWhenWakeup(request.getClientChannel(), + request.getRequestCommand()); + } catch (Throwable e) { + log.error( + "PullRequestHoldService#notifyMessageArriving: failed to execute request when " + + "message matched, topic={}, queueId={}", topic, queueId, e); + } + continue; + } + } + + if (System.currentTimeMillis() >= (request.getSuspendTimestamp() + request.getTimeoutMillis())) { + try { + this.brokerController.getPullMessageProcessor().executeRequestWhenWakeup(request.getClientChannel(), + request.getRequestCommand()); + } catch (Throwable e) { + log.error( + "PullRequestHoldService#notifyMessageArriving: failed to execute request when time's " + + "up, topic={}, queueId={}", topic, queueId, e); + } + continue; + } + + replayList.add(request); + } + + if (!replayList.isEmpty()) { + mpr.addPullRequest(replayList); + } + } + } + } + + 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 0000000..4b319f1 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/metrics/BrokerMetricsConstant.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.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 GAUGE_TOPIC_NUM = "rocketmq_topic_number"; + public static final String GAUGE_CONSUMER_GROUP_NUM = "rocketmq_consumer_group_number"; + + 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 HISTOGRAM_TOPIC_CREATE_EXECUTE_TIME = "rocketmq_topic_create_execution_time"; + public static final String HISTOGRAM_CONSUMER_GROUP_CREATE_EXECUTE_TIME = "rocketmq_consumer_group_create_execution_time"; + + 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_INVOCATION_STATUS = "invocation_status"; + 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 0000000..d8d94f8 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/metrics/BrokerMetricsManager.java @@ -0,0 +1,702 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.time.Duration; +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_TOPIC_NUM; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.GAUGE_CONSUMER_GROUP_NUM; +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.HISTOGRAM_TOPIC_CREATE_EXECUTE_TIME; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.HISTOGRAM_CONSUMER_GROUP_CREATE_EXECUTE_TIME; +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(); + public static ObservableLongGauge topicNum = new NopObservableLongGauge(); + public static ObservableLongGauge consumerGroupNum = 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(); + public static LongHistogram topicCreateExecuteTime = new NopLongHistogram(); + public static LongHistogram consumerGroupCreateExecuteTime = 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 + ); + + List createTimeBuckets = Arrays.asList( + (double) Duration.ofMillis(10).toMillis(), //10ms + (double) Duration.ofMillis(100).toMillis(), //100ms + (double) Duration.ofSeconds(1).toMillis(), //1s + (double) Duration.ofSeconds(3).toMillis(), //3s + (double) Duration.ofSeconds(5).toMillis() //5s + ); + 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()); + + InstrumentSelector createTopicTimeSelector = InstrumentSelector.builder() + .setType(InstrumentType.HISTOGRAM) + .setName(HISTOGRAM_TOPIC_CREATE_EXECUTE_TIME) + .build(); + InstrumentSelector createSubGroupTimeSelector = InstrumentSelector.builder() + .setType(InstrumentType.HISTOGRAM) + .setName(HISTOGRAM_CONSUMER_GROUP_CREATE_EXECUTE_TIME) + .build(); + ViewBuilder createTopicTimeViewBuilder = View.builder() + .setAggregation(Aggregation.explicitBucketHistogram(createTimeBuckets)); + ViewBuilder createSubGroupTimeViewBuilder = View.builder() + .setAggregation(Aggregation.explicitBucketHistogram(createTimeBuckets)); + // To config the cardinalityLimit for openTelemetry metrics exporting. + SdkMeterProviderUtil.setCardinalityLimit(createTopicTimeViewBuilder, brokerConfig.getMetricsOtelCardinalityLimit()); + providerBuilder.registerView(createTopicTimeSelector, createTopicTimeViewBuilder.build()); + SdkMeterProviderUtil.setCardinalityLimit(createSubGroupTimeViewBuilder, brokerConfig.getMetricsOtelCardinalityLimit()); + providerBuilder.registerView(createSubGroupTimeSelector, createSubGroupTimeViewBuilder.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())); + + topicNum = brokerMeter.gaugeBuilder(GAUGE_TOPIC_NUM) + .setDescription("Active topic number") + .ofLongs() + .buildWithCallback(measurement -> measurement.record(brokerController.getTopicConfigManager().getTopicConfigTable().size(), newAttributesBuilder().build())); + + consumerGroupNum = brokerMeter.gaugeBuilder(GAUGE_CONSUMER_GROUP_NUM) + .setDescription("Active subscription group number") + .ofLongs() + .buildWithCallback(measurement -> measurement.record(brokerController.getSubscriptionGroupManager().getSubscriptionGroupTable().size(), 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(); + + topicCreateExecuteTime = brokerMeter.histogramBuilder(HISTOGRAM_TOPIC_CREATE_EXECUTE_TIME) + .setDescription("The distribution of create topic time") + .ofLongs() + .setUnit("milliseconds") + .build(); + + consumerGroupCreateExecuteTime = brokerMeter.histogramBuilder(HISTOGRAM_CONSUMER_GROUP_CREATE_EXECUTE_TIME) + .setDescription("The distribution of create subscription time") + .ofLongs() + .setUnit("milliseconds") + .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 0000000..28f36cc --- /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 0000000..35519c1 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/metrics/ConsumerLagCalculator.java @@ -0,0 +1,527 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.longpolling.PopCommandCallback; +import org.apache.rocketmq.broker.longpolling.PopLongPollingService; +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; +import org.apache.rocketmq.store.exception.ConsumeQueueException; + +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 PopLongPollingService popLongPollingService; + 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.popLongPollingService = brokerController.getPopMessageProcessor().getPopLongPollingService(); + this.popInflightMessageCounter = brokerController.getPopInflightMessageCounter(); + } + + public 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; + } + + if (info.isPop && brokerConfig.isEnableNotifyBeforePopCalculateLag()) { + if (popLongPollingService.notifyMessageArriving(info.topic, -1, info.group, + true, null, 0, null, null, new PopCommandCallback(this::calculate, info, lagRecorder))) { + return; + } + } + + calculate(info, lagRecorder); + }); + } + + public void calculate(ProcessGroupInfo info, Consumer lagRecorder) { + CalculateLagResult result = new CalculateLagResult(info.group, info.topic, false); + try { + Pair lag = getConsumerLagStats(info.group, info.topic, info.isPop); + if (lag != null) { + result.lag = lag.getObject1(); + result.earliestUnconsumedTimestamp = lag.getObject2(); + } + lagRecorder.accept(result); + } catch (ConsumeQueueException e) { + LOGGER.error("Failed to get lag stats", e); + } + + if (info.isPop) { + try { + 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); + } catch (ConsumeQueueException e) { + LOGGER.error("Failed to get lag stats", e); + } + } + } + + public void calculateInflight(Consumer inflightRecorder) { + processAllGroup(info -> { + CalculateInflightResult result = new CalculateInflightResult(info.group, info.topic, false); + try { + Pair inFlight = getInFlightMsgStats(info.group, info.topic, info.isPop); + if (inFlight != null) { + result.inFlight = inFlight.getObject1(); + result.earliestUnPulledTimestamp = inFlight.getObject2(); + } + inflightRecorder.accept(result); + } catch (ConsumeQueueException e) { + LOGGER.error("Failed to get inflight message stats", e); + } + + if (info.isPop) { + try { + 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); + } catch (ConsumeQueueException e) { + LOGGER.error("Failed to get inflight message stats", e); + } + } + }); + } + + public void calculateAvailable(Consumer availableRecorder) { + processAllGroup(info -> { + CalculateAvailableResult result = new CalculateAvailableResult(info.group, info.topic, false); + + try { + result.available = getAvailableMsgCount(info.group, info.topic, info.isPop); + availableRecorder.accept(result); + } catch (ConsumeQueueException e) { + LOGGER.error("Failed to get available message count", e); + } + + + if (info.isPop) { + try { + long retryAvailable = getAvailableMsgCount(info.group, info.retryTopic, true); + result = new CalculateAvailableResult(info.group, info.topic, true); + result.available = retryAvailable; + availableRecorder.accept(result); + } catch (ConsumeQueueException e) { + LOGGER.error("Failed to get available message count", e); + } + } + }); + } + + public Pair getConsumerLagStats(String group, String topic, boolean isPop) throws ConsumeQueueException { + 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; + } + + LOGGER.debug("GetConsumerLagStats, topic={}, group={}, lag={}, latency={}", topic, group, total, + earliestUnconsumedTimestamp > 0 ? System.currentTimeMillis() - earliestUnconsumedTimestamp : 0); + + return new Pair<>(total, earliestUnconsumedTimestamp); + } + + public Pair getConsumerLagStats(String group, String topic, int queueId, boolean isPop) + throws ConsumeQueueException { + long brokerOffset = messageStore.getMaxOffsetInQueue(topic, queueId); + if (brokerOffset < 0) { + brokerOffset = 0; + } + + if (isPop && !brokerConfig.isPopConsumerKVServiceEnable()) { + 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) throws ConsumeQueueException { + 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) + throws ConsumeQueueException { + if (isPop && !brokerConfig.isPopConsumerKVServiceEnable()) { + 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) throws ConsumeQueueException { + 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) + throws ConsumeQueueException { + long brokerOffset = messageStore.getMaxOffsetInQueue(topic, queueId); + if (brokerOffset < 0) { + brokerOffset = 0; + } + + long pullOffset; + if (isPop && !brokerConfig.isPopConsumerKVServiceEnable()) { + pullOffset = popBufferMergeService.getLatestOffset(topic, group, queueId); + if (pullOffset < 0) { + pullOffset = offsetManager.queryOffset(group, topic, queueId); + } + } 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/InvocationStatus.java b/broker/src/main/java/org/apache/rocketmq/broker/metrics/InvocationStatus.java new file mode 100644 index 0000000..c7501e5 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/metrics/InvocationStatus.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 enum InvocationStatus { + SUCCESS("success"), + FAILURE("failure"); + + private final String name; + + InvocationStatus(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} \ No newline at end of file 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 0000000..41917ed --- /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 0000000..6e87cb0 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/metrics/PopMetricsManager.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.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.exception.ConsumeQueueException; +import org.apache.rocketmq.store.pop.AckMsg; +import org.apache.rocketmq.store.pop.PopCheckPoint; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +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 { + private static final Logger log = LoggerFactory.getLogger(PopMetricsManager.class); + 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) { + try { + measurement.record(popReviveService.getReviveBehindMillis(), newAttributesBuilder() + .put(LABEL_QUEUE_ID, popReviveService.getQueueId()) + .build()); + } catch (ConsumeQueueException e) { + log.error("Failed to get revive behind duration", e); + } + } + } + + private static void calculatePopReviveLag(BrokerController brokerController, + ObservableLongMeasurement measurement) { + PopReviveService[] popReviveServices = brokerController.getAckMessageProcessor().getPopReviveServices(); + for (PopReviveService popReviveService : popReviveServices) { + try { + measurement.record(popReviveService.getReviveBehindMessages(), newAttributesBuilder() + .put(LABEL_QUEUE_ID, popReviveService.getQueueId()) + .build()); + } catch (ConsumeQueueException e) { + log.error("Failed to get revive behind message count", e); + } + } + } + + 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 0000000..3f6fe9c --- /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 0000000..d40aba2 --- /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 new file mode 100644 index 0000000..e45f48f --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/mqtrace/ConsumeMessageContext.java @@ -0,0 +1,243 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mqtrace; + +import java.util.Map; + +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.store.stats.BrokerStatsManager; + +public class ConsumeMessageContext { + private String consumerGroup; + private String topic; + private Integer queueId; + private String clientHost; + private String storeHost; + private Map messageIds; + private int bodyLength; + 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 int filterMessageCount; + + private String namespace; + 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 String getClientHost() { + return clientHost; + } + + public void setClientHost(String clientHost) { + this.clientHost = clientHost; + } + + public String getStoreHost() { + return storeHost; + } + + public void setStoreHost(String storeHost) { + this.storeHost = storeHost; + } + + public Map getMessageIds() { + return messageIds; + } + + public void setMessageIds(Map messageIds) { + this.messageIds = messageIds; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Object getMqTraceContext() { + return mqTraceContext; + } + + 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; + } + + 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; + } + + public void setCommercialOwner(final String commercialOwner) { + this.commercialOwner = commercialOwner; + } + + public BrokerStatsManager.StatsType getCommercialRcvStats() { + return commercialRcvStats; + } + + public void setCommercialRcvStats(final BrokerStatsManager.StatsType commercialRcvStats) { + this.commercialRcvStats = commercialRcvStats; + } + + public int getCommercialRcvTimes() { + return commercialRcvTimes; + } + + public void setCommercialRcvTimes(final int commercialRcvTimes) { + this.commercialRcvTimes = commercialRcvTimes; + } + + public int getCommercialRcvSize() { + return commercialRcvSize; + } + + public void setCommercialRcvSize(final int commercialRcvSize) { + this.commercialRcvSize = commercialRcvSize; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public int getFilterMessageCount() { + return filterMessageCount; + } + + public void setFilterMessageCount(int filterMessageCount) { + this.filterMessageCount = filterMessageCount; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/mqtrace/ConsumeMessageHook.java b/broker/src/main/java/org/apache/rocketmq/broker/mqtrace/ConsumeMessageHook.java new file mode 100644 index 0000000..9db8536 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/mqtrace/ConsumeMessageHook.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.mqtrace; + +public interface ConsumeMessageHook { + String hookName(); + + void consumeMessageBefore(final ConsumeMessageContext context); + + void consumeMessageAfter(final ConsumeMessageContext context); +} 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 new file mode 100644 index 0000000..aaa84b7 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/mqtrace/SendMessageContext.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.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; + private Integer queueId; + private Long queueOffset; + private String brokerAddr; + private String bornHost; + private int bodyLength; + private int code; + private String errorMsg; + private String msgProps; + private Object mqTraceContext; + private Properties extProps; + private String brokerRegionId; + private String msgUniqueKey; + private long bornTimeStamp; + private long requestTimeStamp; + private MessageType msgType = MessageType.Trans_msg_Commit; + + private boolean isSuccess = false; + + /** + * 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; + } + + public void setSuccess(final boolean success) { + isSuccess = success; + } + + public MessageType getMsgType() { + return msgType; + } + + public void setMsgType(final MessageType msgType) { + this.msgType = msgType; + } + + public String getMsgUniqueKey() { + return msgUniqueKey; + } + + public void setMsgUniqueKey(final String msgUniqueKey) { + this.msgUniqueKey = msgUniqueKey; + } + + public long getBornTimeStamp() { + return bornTimeStamp; + } + + 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; + } + + public void setBrokerRegionId(final String brokerRegionId) { + this.brokerRegionId = brokerRegionId; + } + + 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 getMsgId() { + return msgId; + } + + public void setMsgId(String msgId) { + this.msgId = msgId; + } + + public String getOriginMsgId() { + return originMsgId; + } + + public void setOriginMsgId(String originMsgId) { + this.originMsgId = originMsgId; + } + + 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 getBrokerAddr() { + return brokerAddr; + } + + public void setBrokerAddr(String brokerAddr) { + this.brokerAddr = brokerAddr; + } + + public String getBornHost() { + return bornHost; + } + + public void setBornHost(String bornHost) { + this.bornHost = bornHost; + } + + public int getBodyLength() { + return bodyLength; + } + + public void setBodyLength(int bodyLength) { + this.bodyLength = bodyLength; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getErrorMsg() { + return errorMsg; + } + + public void setErrorMsg(String errorMsg) { + this.errorMsg = errorMsg; + } + + public String getMsgProps() { + return msgProps; + } + + public void setMsgProps(String msgProps) { + this.msgProps = msgProps; + } + + public Object getMqTraceContext() { + return mqTraceContext; + } + + public void setMqTraceContext(Object mqTraceContext) { + this.mqTraceContext = mqTraceContext; + } + + public Properties getExtProps() { + return extProps; + } + + public void setExtProps(Properties extProps) { + this.extProps = extProps; + } + + public String getCommercialOwner() { + return commercialOwner; + } + + 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; + } + + public int getCommercialSendSize() { + return commercialSendSize; + } + + public void setCommercialSendSize(final int commercialSendSize) { + this.commercialSendSize = commercialSendSize; + } + + public int getCommercialSendTimes() { + return commercialSendTimes; + } + + public void setCommercialSendTimes(final int commercialSendTimes) { + this.commercialSendTimes = commercialSendTimes; + } +} 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 new file mode 100644 index 0000000..a89bace --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/mqtrace/SendMessageHook.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.mqtrace; + +public interface SendMessageHook { + String hookName(); + + void sendMessageBefore(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 0000000..79bb0c7 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/offset/BroadcastOffsetManager.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.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; +import org.apache.rocketmq.store.exception.ConsumeQueueException; + +/** + * 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) throws ConsumeQueueException { + + 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); + BroadcastTimedOffsetStore offsetStore = broadcastOffsetData.clientOffsetStore.get(clientId); + if (offsetStore == null) { + offsetStore = new BroadcastTimedOffsetStore(fromProxy); + broadcastOffsetData.clientOffsetStore.put(clientId, offsetStore); + } + + 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)); + } else { + if (offsetStore.fromProxy != fromProxy) { + offset.set(getOffset(offsetStore, topic, groupId, queueId)); + } + } + return offset.get(); + } + + private long getOffset(BroadcastTimedOffsetStore offsetStore, String topic, String groupId, int queueId) + throws ConsumeQueueException { + 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 0000000..3770e57 --- /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 new file mode 100644 index 0000000..140604f --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/offset/ConsumerOffsetManager.java @@ -0,0 +1,464 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.collect.Maps; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +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.atomic.AtomicLong; + +import com.google.common.base.Strings; + +import java.util.function.Function; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.BrokerPathConfigHelper; +import org.apache.rocketmq.common.ConfigManager; +import org.apache.rocketmq.common.MixAll; +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; + +public class ConsumerOffsetManager extends ConfigManager { + protected static final Logger LOG = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + public static final String TOPIC_GROUP_SEPARATOR = "@"; + + protected DataVersion dataVersion = new DataVersion(); + + protected ConcurrentMap> offsetTable = + new ConcurrentHashMap<>(512); + + private final ConcurrentMap> resetOffsetTable = + new ConcurrentHashMap<>(512); + + private final ConcurrentMap> pullOffsetTable = + new ConcurrentHashMap<>(512); + + protected transient BrokerController brokerController; + + private final transient AtomicLong versionChangeCounter = new AtomicLong(0); + + public ConsumerOffsetManager() { + } + + 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()) { + Entry> next = it.next(); + String topicAtGroup = next.getKey(); + String[] arrays = topicAtGroup.split(TOPIC_GROUP_SEPARATOR); + if (arrays.length == 2) { + String topic = arrays[0]; + String group = arrays[1]; + + if (null == brokerController.getConsumerManager().findSubscriptionData(group, topic) + && this.offsetBehindMuchThanData(topic, next.getValue())) { + it.remove(); + removeConsumerOffset(topicAtGroup); + LOG.warn("remove topic offset, {}", topicAtGroup); + } + } + } + } + + private boolean offsetBehindMuchThanData(final String topic, ConcurrentMap table) { + Iterator> it = table.entrySet().iterator(); + boolean result = !table.isEmpty(); + + while (it.hasNext() && result) { + Entry next = it.next(); + long minOffsetInStore = this.brokerController.getMessageStore().getMinOffsetInQueue(topic, next.getKey()); + long offsetInPersist = next.getValue(); + result = offsetInPersist <= minOffsetInStore; + } + + return result; + } + + public Set whichTopicByConsumer(final String group) { + Set topics = new HashSet<>(); + + Iterator>> it = this.offsetTable.entrySet().iterator(); + while (it.hasNext()) { + Entry> next = it.next(); + String topicAtGroup = next.getKey(); + String[] arrays = topicAtGroup.split(TOPIC_GROUP_SEPARATOR); + if (arrays.length == 2) { + if (group.equals(arrays[1])) { + topics.add(arrays[0]); + } + } + } + + return topics; + } + + public Set whichGroupByTopic(final String topic) { + Set groups = new HashSet<>(); + + Iterator>> it = this.offsetTable.entrySet().iterator(); + while (it.hasNext()) { + Entry> next = it.next(); + String topicAtGroup = next.getKey(); + String[] arrays = topicAtGroup.split(TOPIC_GROUP_SEPARATOR); + if (arrays.length == 2) { + if (topic.equals(arrays[0])) { + groups.add(arrays[1]); + } + } + } + + 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 + String key = topic + TOPIC_GROUP_SEPARATOR + group; + this.commitOffset(clientHost, key, queueId, offset); + } + + 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.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); + } + } + 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) { + 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); + } + + if (offset == null) { + offset = queryOffset(group, topic, queueId); + } + + return offset; + } + + public void clearPullOffset(final String group, final String topic) { + this.pullOffsetTable.remove(topic + TOPIC_GROUP_SEPARATOR + group); + } + + @Override + public String encode() { + return this.encode(false); + } + + @Override + public String configFilePath() { + return BrokerPathConfigHelper.getConsumerOffsetPath(this.brokerController.getMessageStoreConfig().getStorePathRootDir()); + } + + @Override + public void decode(String jsonString) { + if (jsonString != null) { + ConsumerOffsetManager obj = RemotingSerializable.fromJson(jsonString, ConsumerOffsetManager.class); + if (obj != null) { + this.setOffsetTable(obj.getOffsetTable()); + this.dataVersion = obj.dataVersion; + } + } + } + + @Override + public String encode(final boolean prettyFormat) { + return RemotingSerializable.toJson(this, prettyFormat); + } + + public ConcurrentMap> getOffsetTable() { + return offsetTable; + } + + public void setOffsetTable(ConcurrentMap> offsetTable) { + this.offsetTable = offsetTable; + } + + public Map queryMinOffsetInAllGroup(final String topic, final String filterGroups) { + + 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()) { + String topicAtGroup = it.next(); + if (group.equals(topicAtGroup.split(TOPIC_GROUP_SEPARATOR)[1])) { + it.remove(); + removeConsumerOffset(topicAtGroup); + } + } + } + } + + for (Map.Entry> offSetEntry : this.offsetTable.entrySet()) { + String topicGroup = offSetEntry.getKey(); + String[] topicGroupArr = topicGroup.split(TOPIC_GROUP_SEPARATOR); + if (topic.equals(topicGroupArr[0])) { + for (Entry entry : offSetEntry.getValue().entrySet()) { + long minOffset = this.brokerController.getMessageStore().getMinOffsetInQueue(topic, entry.getKey()); + if (entry.getValue() >= minOffset) { + Long offset = queueMinOffset.get(entry.getKey()); + if (offset == null) { + queueMinOffset.put(entry.getKey(), Math.min(Long.MAX_VALUE, entry.getValue())); + } else { + queueMinOffset.put(entry.getKey(), Math.min(entry.getValue(), offset)); + } + } + } + } + + } + return queueMinOffset; + } + + public Map queryOffset(final String group, final String topic) { + // topic@group + String key = topic + TOPIC_GROUP_SEPARATOR + group; + return this.offsetTable.get(key); + } + + 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)); + } + } + + public DataVersion getDataVersion() { + return dataVersion; + } + + public void setDataVersion(DataVersion dataVersion) { + this.dataVersion = dataVersion; + } + + public boolean loadDataVersion() { + String fileName = null; + try { + fileName = this.configFilePath(); + String jsonString = MixAll.file2String(fileName); + if (jsonString != null) { + ConsumerOffsetManager obj = RemotingSerializable.fromJson(jsonString, ConsumerOffsetManager.class); + if (obj != null) { + this.dataVersion = obj.dataVersion; + } + LOG.info("load consumer offset dataVersion success,{},{} ", fileName, jsonString); + } + return true; + } catch (Exception e) { + LOG.error("load consumer offset dataVersion failed " + fileName, e); + return false; + } + } + + public void removeOffset(final String group) { + Function>>, Boolean> deleteFunction = it -> { + boolean removed = false; + while (it.hasNext()) { + Entry> entry = it.next(); + String topicAtGroup = entry.getKey(); + if (topicAtGroup.contains(group)) { + String[] arrays = topicAtGroup.split(TOPIC_GROUP_SEPARATOR); + if (arrays.length == 2 && group.equals(arrays[1])) { + it.remove(); + removeConsumerOffset(topicAtGroup); + removed = true; + } + } + } + return removed; + }; + + boolean clearOffset = deleteFunction.apply(this.offsetTable.entrySet().iterator()); + boolean clearReset = deleteFunction.apply(this.resetOffsetTable.entrySet().iterator()); + boolean clearPull = deleteFunction.apply(this.pullOffsetTable.entrySet().iterator()); + + LOG.info("Consumer offset manager clean group offset, groupName={}, " + + "offsetTable={}, resetOffsetTable={}, pullOffsetTable={}", group, clearOffset, clearReset, clearPull); + } + + 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; + resetOffsetTable.computeIfAbsent(key, k -> Maps.newConcurrentMap()).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. + offsetTable.computeIfAbsent(key, k -> Maps.newConcurrentMap()).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 0000000..37b3eed --- /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 0000000..120f5b1 --- /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().containsSubscriptionGroup(group)) { + 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 0000000..53e9e2e --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/offset/LmqConsumerOffsetManager.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.offset; + +import java.util.HashMap; +import java.util.Iterator; +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; + } + + @Override + public void removeOffset(String group) { + if (!MixAll.isLmq(group)) { + super.removeOffset(group); + return; + } + Iterator> it = this.lmqOffsetTable.entrySet().iterator(); + while (it.hasNext()) { + Map.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 lmq group offset {}", topicAtGroup); + } + } + } + } +} 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 new file mode 100644 index 0000000..83edd88 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/out/BrokerOuterAPI.java @@ -0,0 +1,1494 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.out; + +import com.alibaba.fastjson2.JSON; +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.commons.lang3.tuple.Triple; +import org.apache.rocketmq.acl.common.AclClientRPCHook; +import org.apache.rocketmq.acl.common.SessionCredentials; +import org.apache.rocketmq.auth.config.AuthConfig; +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.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.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.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 LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + private final RemotingClient remotingClient; + 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, AuthConfig authConfig) { + this(nettyClientConfig, authConfig, new DynamicalExtFieldRPCHook(), new ClientMetadata()); + } + + private BrokerOuterAPI(final NettyClientConfig nettyClientConfig, AuthConfig authConfig, RPCHook rpcHook, ClientMetadata clientMetadata) { + this.remotingClient = new NettyRemotingClient(nettyClientConfig); + this.clientMetadata = clientMetadata; + this.remotingClient.registerRPCHook(rpcHook); + this.remotingClient.registerRPCHook(newAclRPCHook(authConfig)); + this.rpcClient = new RpcClientImpl(this.clientMetadata, this.remotingClient); + } + + private RPCHook newAclRPCHook(AuthConfig config) { + if (config == null || StringUtils.isBlank(config.getInnerClientAuthenticationCredentials())) { + return null; + } + SessionCredentials sessionCredentials = + JSON.parseObject(config.getInnerClientAuthenticationCredentials(), SessionCredentials.class); + if (StringUtils.isBlank(sessionCredentials.getAccessKey()) || StringUtils.isBlank(sessionCredentials.getSecretKey())) { + return null; + } + return new AclClientRPCHook(sessionCredentials); + } + + public void start() { + this.remotingClient.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 (!UtilAll.isBlank(addrs)) { + if (!addrs.equals(this.nameSrvAddr)) { + LOGGER.info("name server address changed, old: {} new: {}", this.nameSrvAddr, addrs); + this.updateNameServerAddressList(addrs); + this.nameSrvAddr = addrs; + return nameSrvAddr; + } + } + } catch (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) { + String[] addrArray = addrs.split(";"); + 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 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.isEmpty()) { + 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 = 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, + 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 BrokerIdentity brokerIdentity) { + return registerBrokerAll(clusterName, + brokerAddr, + brokerName, + brokerId, + haServerAddr, + topicConfigWrapper, + filterServerList, + oneway, timeoutMills, + enableActingMaster, + compressed, + null, + brokerIdentity); + } + + /** + * 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(); + } + } + }); + } + + 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 registerBrokerResultList; + } + + private RegisterBrokerResult registerBroker( + final String namesrvAddr, + final boolean oneway, + final int timeoutMills, + final RegisterBrokerRequestHeader requestHeader, + final byte[] body + ) throws RemotingCommandException, MQBrokerException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, + InterruptedException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.REGISTER_BROKER, requestHeader); + request.setBody(body); + + if (oneway) { + try { + this.remotingClient.invokeOneway(namesrvAddr, request, timeoutMills); + } catch (RemotingTooMuchRequestException e) { + // Ignore + } + return null; + } + + RemotingCommand response = this.remotingClient.invokeSync(namesrvAddr, request, timeoutMills); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + RegisterBrokerResponseHeader responseHeader = response.decodeCommandCustomHeader(RegisterBrokerResponseHeader.class); + RegisterBrokerResult result = new RegisterBrokerResult(); + result.setMasterAddr(responseHeader.getMasterAddr()); + result.setHaServerAddr(responseHeader.getHaServerAddr()); + if (response.getBody() != null) { + result.setKvTable(KVTable.decode(response.getBody(), KVTable.class)); + } + return result; + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark(), requestHeader == null ? null : requestHeader.getBrokerAddr()); + } + + public void unregisterBrokerAll( + final String clusterName, + final String brokerAddr, + final String brokerName, + final long brokerId + ) { + List nameServerAddressList = this.remotingClient.getNameServerAddressList(); + if (nameServerAddressList != null) { + for (String namesrvAddr : nameServerAddressList) { + try { + this.unregisterBroker(namesrvAddr, clusterName, brokerAddr, brokerName, brokerId); + LOGGER.info("unregisterBroker OK, NamesrvAddr: {}", namesrvAddr); + } catch (Exception e) { + LOGGER.warn("unregisterBroker Exception, NamesrvAddr: {}", namesrvAddr, e); + } + } + } + } + + public void unregisterBroker( + final String namesrvAddr, + final String clusterName, + final String brokerAddr, + final String brokerName, + final long brokerId + ) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException { + UnRegisterBrokerRequestHeader requestHeader = new UnRegisterBrokerRequestHeader(); + requestHeader.setBrokerAddr(brokerAddr); + requestHeader.setBrokerId(brokerId); + requestHeader.setBrokerName(brokerName); + requestHeader.setClusterName(clusterName); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UNREGISTER_BROKER, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(namesrvAddr, request, 3000); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return; + } + default: + break; + } + + 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 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 = + 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); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(true, addr), request, 3000); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return TopicConfigSerializeWrapper.decode(response.getBody(), TopicConfigAndMappingSerializeWrapper.class); + } + default: + break; + } + + 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( + final String addr) throws InterruptedException, RemotingTimeoutException, + RemotingSendRequestException, RemotingConnectException, MQBrokerException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ALL_CONSUMER_OFFSET, null); + RemotingCommand response = this.remotingClient.invokeSync(addr, request, 3000); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return ConsumerOffsetSerializeWrapper.decode(response.getBody(), ConsumerOffsetSerializeWrapper.class); + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + + public String getAllDelayOffset( + final String addr) throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, + RemotingConnectException, MQBrokerException, UnsupportedEncodingException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ALL_DELAY_OFFSET, null); + RemotingCommand response = this.remotingClient.invokeSync(addr, request, 3000); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return new String(response.getBody(), MixAll.DEFAULT_CHARSET); + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + + public SubscriptionGroupWrapper getAllSubscriptionGroupConfig( + final String addr) throws InterruptedException, RemotingTimeoutException, + RemotingSendRequestException, RemotingConnectException, MQBrokerException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ALL_SUBSCRIPTIONGROUP_CONFIG, null); + RemotingCommand response = this.remotingClient.invokeSync(addr, request, 3000); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return SubscriptionGroupWrapper.decode(response.getBody(), SubscriptionGroupWrapper.class); + } + default: + break; + } + + 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 = 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 = 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 = 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 = 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 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 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 = 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 = 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); + } + } + }); + } + + // Triple, should check info and retry if and only if PullResult is null + 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(Triple.of(pullResultExt, pullResultExt.getPullStatus().name(), false)); // found or not found really, so no retry + } catch (Exception e) { + // retry when NO_PERMISSION, SUBSCRIPTION_GROUP_NOT_EXIST etc. even when TOPIC_NOT_EXIST + pullResultFuture.complete(Triple.of(null, "Response Code:" + response.getCode(), true)); + } + } + + @Override + public void operationFail(Throwable throwable) { + pullResultFuture.complete(Triple.of(null, throwable.getMessage(), true)); + } + }); + 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 = 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 new file mode 100644 index 0000000..373a0a4 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/pagecache/ManyMessageTransfer.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.broker.pagecache; + +import io.netty.channel.FileRegion; +import io.netty.util.AbstractReferenceCounted; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.WritableByteChannel; +import java.util.List; +import org.apache.rocketmq.store.GetMessageResult; + +public class ManyMessageTransfer extends AbstractReferenceCounted implements FileRegion { + private final ByteBuffer byteBufferHeader; + private final GetMessageResult getMessageResult; + + /** + * Bytes which were transferred already. + */ + private long transferred; + + public ManyMessageTransfer(ByteBuffer byteBufferHeader, GetMessageResult getMessageResult) { + this.byteBufferHeader = byteBufferHeader; + this.getMessageResult = getMessageResult; + } + + @Override + public long position() { + int pos = byteBufferHeader.position(); + List messageBufferList = this.getMessageResult.getMessageBufferList(); + for (ByteBuffer bb : messageBufferList) { + pos += bb.position(); + } + return pos; + } + + @Override + public long transfered() { + return transferred; + } + + @Override + public long transferred() { + return transferred; + } + + @Override + public long count() { + return byteBufferHeader.limit() + this.getMessageResult.getBufferTotalSize(); + } + + @Override + public long transferTo(WritableByteChannel target, long position) throws IOException { + if (this.byteBufferHeader.hasRemaining()) { + transferred += target.write(this.byteBufferHeader); + return transferred; + } else { + List messageBufferList = this.getMessageResult.getMessageBufferList(); + for (ByteBuffer bb : messageBufferList) { + if (bb.hasRemaining()) { + transferred += target.write(bb); + return transferred; + } + } + } + + 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(); + } + + @Override + protected void deallocate() { + this.getMessageResult.release(); + } +} 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 new file mode 100644 index 0000000..952cf4f --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/pagecache/OneMessageTransfer.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.broker.pagecache; + +import io.netty.channel.FileRegion; +import io.netty.util.AbstractReferenceCounted; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.WritableByteChannel; +import org.apache.rocketmq.store.SelectMappedBufferResult; + +public class OneMessageTransfer extends AbstractReferenceCounted implements FileRegion { + private final ByteBuffer byteBufferHeader; + private final SelectMappedBufferResult selectMappedBufferResult; + + /** + * Bytes which were transferred already. + */ + private long transferred; + + public OneMessageTransfer(ByteBuffer byteBufferHeader, SelectMappedBufferResult selectMappedBufferResult) { + this.byteBufferHeader = byteBufferHeader; + this.selectMappedBufferResult = selectMappedBufferResult; + } + + @Override + public long position() { + return this.byteBufferHeader.position() + this.selectMappedBufferResult.getByteBuffer().position(); + } + + @Override + public long transfered() { + return transferred; + } + + @Override + public long transferred() { + return transferred; + } + + @Override + public long count() { + return this.byteBufferHeader.limit() + this.selectMappedBufferResult.getSize(); + } + + @Override + public long transferTo(WritableByteChannel target, long position) throws IOException { + if (this.byteBufferHeader.hasRemaining()) { + transferred += target.write(this.byteBufferHeader); + return transferred; + } else if (this.selectMappedBufferResult.getByteBuffer().hasRemaining()) { + transferred += target.write(this.selectMappedBufferResult.getByteBuffer()); + return transferred; + } + + 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(); + } + + @Override + protected void deallocate() { + this.selectMappedBufferResult.release(); + } +} 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 new file mode 100644 index 0000000..db47b9e --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/pagecache/QueryMessageTransfer.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.broker.pagecache; + +import io.netty.channel.FileRegion; +import io.netty.util.AbstractReferenceCounted; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.WritableByteChannel; +import java.util.List; +import org.apache.rocketmq.store.QueryMessageResult; + +public class QueryMessageTransfer extends AbstractReferenceCounted implements FileRegion { + private final ByteBuffer byteBufferHeader; + private final QueryMessageResult queryMessageResult; + + /** + * Bytes which were transferred already. + */ + private long transferred; + + public QueryMessageTransfer(ByteBuffer byteBufferHeader, QueryMessageResult queryMessageResult) { + this.byteBufferHeader = byteBufferHeader; + this.queryMessageResult = queryMessageResult; + } + + @Override + public long position() { + int pos = byteBufferHeader.position(); + List messageBufferList = this.queryMessageResult.getMessageBufferList(); + for (ByteBuffer bb : messageBufferList) { + pos += bb.position(); + } + return pos; + } + + @Override + public long transfered() { + return transferred; + } + + @Override + public long transferred() { + return transferred; + } + + @Override + public long count() { + return byteBufferHeader.limit() + this.queryMessageResult.getBufferTotalSize(); + } + + @Override + public long transferTo(WritableByteChannel target, long position) throws IOException { + if (this.byteBufferHeader.hasRemaining()) { + transferred += target.write(this.byteBufferHeader); + return transferred; + } else { + List messageBufferList = this.queryMessageResult.getMessageBufferList(); + for (ByteBuffer bb : messageBufferList) { + if (bb.hasRemaining()) { + transferred += target.write(bb); + return transferred; + } + } + } + + 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(); + } + + @Override + protected void deallocate() { + this.queryMessageResult.release(); + } +} 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 0000000..0cd2a27 --- /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 0000000..bddb57f --- /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/pop/PopConsumerCache.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerCache.java new file mode 100644 index 0000000..e7ce68e --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerCache.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.broker.pop; + +import java.util.ArrayList; +import java.util.Iterator; +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.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.offset.ConsumerOffsetManager; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.ConcurrentHashMapUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PopConsumerCache extends ServiceThread { + + private static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); + private static final long OFFSET_NOT_EXIST = -1L; + + private final BrokerController brokerController; + private final PopConsumerKVStore consumerRecordStore; + private final PopConsumerLockService consumerLockService; + private final Consumer reviveConsumer; + + private final AtomicInteger estimateCacheSize; + private final ConcurrentMap consumerRecordTable; + + public PopConsumerCache(BrokerController brokerController, PopConsumerKVStore consumerRecordStore, + PopConsumerLockService popConsumerLockService, Consumer reviveConsumer) { + + this.reviveConsumer = reviveConsumer; + this.brokerController = brokerController; + this.consumerRecordStore = consumerRecordStore; + this.consumerLockService = popConsumerLockService; + this.estimateCacheSize = new AtomicInteger(); + this.consumerRecordTable = new ConcurrentHashMap<>(); + } + + public String getKey(String groupId, String topicId, int queueId) { + return groupId + "@" + topicId + "@" + queueId; + } + + public String getKey(PopConsumerRecord consumerRecord) { + return consumerRecord.getGroupId() + "@" + consumerRecord.getTopicId() + "@" + consumerRecord.getQueueId(); + } + + public int getCacheKeySize() { + return this.consumerRecordTable.size(); + } + + public int getCacheSize() { + return this.estimateCacheSize.intValue(); + } + + public boolean isCacheFull() { + return this.estimateCacheSize.intValue() > brokerController.getBrokerConfig().getPopCkMaxBufferSize(); + } + + public long getMinOffsetInCache(String groupId, String topicId, int queueId) { + ConsumerRecords consumerRecords = consumerRecordTable.get(this.getKey(groupId, topicId, queueId)); + return consumerRecords != null ? consumerRecords.getMinOffsetInBuffer() : OFFSET_NOT_EXIST; + } + + public long getPopInFlightMessageCount(String groupId, String topicId, int queueId) { + ConsumerRecords consumerRecords = consumerRecordTable.get(this.getKey(groupId, topicId, queueId)); + return consumerRecords != null ? consumerRecords.getInFlightRecordCount() : 0L; + } + + public void writeRecords(List consumerRecordList) { + this.estimateCacheSize.addAndGet(consumerRecordList.size()); + consumerRecordList.forEach(consumerRecord -> { + ConsumerRecords consumerRecords = ConcurrentHashMapUtils.computeIfAbsent(consumerRecordTable, + this.getKey(consumerRecord), k -> new ConsumerRecords(brokerController.getBrokerConfig(), + consumerRecord.getGroupId(), consumerRecord.getTopicId(), consumerRecord.getQueueId())); + assert consumerRecords != null; + consumerRecords.write(consumerRecord); + }); + } + + /** + * Remove the record from the input list then return the content that has not been deleted + */ + public List deleteRecords(List consumerRecordList) { + int total = consumerRecordList.size(); + List remain = new ArrayList<>(); + consumerRecordList.forEach(consumerRecord -> { + ConsumerRecords consumerRecords = consumerRecordTable.get(this.getKey(consumerRecord)); + if (consumerRecords == null || !consumerRecords.delete(consumerRecord)) { + remain.add(consumerRecord); + } + }); + this.estimateCacheSize.addAndGet(remain.size() - total); + return remain; + } + + public int cleanupRecords(Consumer consumer) { + int remain = 0; + Iterator> iterator = consumerRecordTable.entrySet().iterator(); + while (iterator.hasNext()) { + // revive or write record to store + ConsumerRecords records = iterator.next().getValue(); + boolean timeout = consumerLockService.isLockTimeout( + records.getGroupId(), records.getTopicId()); + + if (timeout) { + List removeExpiredRecords = + records.removeExpiredRecords(Long.MAX_VALUE); + if (removeExpiredRecords != null) { + consumerRecordStore.writeRecords(removeExpiredRecords); + } + log.info("PopConsumerOffline, so clean expire records, groupId={}, topic={}, queueId={}, records={}", + records.getGroupId(), records.getTopicId(), records.getQueueId(), + removeExpiredRecords != null ? removeExpiredRecords.size() : 0); + iterator.remove(); + continue; + } + + long currentTime = System.currentTimeMillis(); + List writeConsumerRecords = new ArrayList<>(); + List consumerRecords = records.removeExpiredRecords(currentTime); + if (consumerRecords != null) { + consumerRecords.forEach(consumerRecord -> { + if (consumerRecord.getVisibilityTimeout() <= currentTime) { + consumer.accept(consumerRecord); + } else { + writeConsumerRecords.add(consumerRecord); + } + }); + } + + // write to store and handle it later + consumerRecordStore.writeRecords(writeConsumerRecords); + + // commit min offset in buffer to offset store + long offset = records.getMinOffsetInBuffer(); + if (offset > OFFSET_NOT_EXIST) { + this.commitOffset("PopConsumerCache", + records.getGroupId(), records.getTopicId(), records.getQueueId(), offset); + } + + remain += records.getInFlightRecordCount(); + } + return remain; + } + + public void commitOffset(String clientHost, String groupId, String topicId, int queueId, long offset) { + if (!consumerLockService.tryLock(groupId, topicId)) { + return; + } + try { + ConsumerOffsetManager consumerOffsetManager = brokerController.getConsumerOffsetManager(); + long commit = consumerOffsetManager.queryOffset(groupId, topicId, queueId); + if (commit != OFFSET_NOT_EXIST && offset < commit) { + log.info("PopConsumerCache, consumer offset less than store, " + + "groupId={}, topicId={}, queueId={}, offset={}", groupId, topicId, queueId, offset); + } + consumerOffsetManager.commitOffset(clientHost, groupId, topicId, queueId, offset); + } finally { + consumerLockService.unlock(groupId, topicId); + } + } + + public void removeRecords(String groupId, String topicId, int queueId) { + this.consumerRecordTable.remove(this.getKey(groupId, topicId, queueId)); + } + + @Override + public String getServiceName() { + return PopConsumerCache.class.getSimpleName(); + } + + @Override + public void run() { + while (!this.isStopped()) { + try { + this.waitForRunning(TimeUnit.SECONDS.toMillis(1)); + int cacheSize = this.cleanupRecords(reviveConsumer); + this.estimateCacheSize.set(cacheSize); + } catch (Exception e) { + log.error("PopConsumerCacheService revive error", e); + } + } + } + + protected static class ConsumerRecords { + + private final Lock lock; + private final String groupId; + private final String topicId; + private final int queueId; + private final BrokerConfig brokerConfig; + private final TreeMap recordTreeMap; + + public ConsumerRecords(BrokerConfig brokerConfig, String groupId, String topicId, int queueId) { + this.groupId = groupId; + this.topicId = topicId; + this.queueId = queueId; + this.lock = new ReentrantLock(); + this.brokerConfig = brokerConfig; + this.recordTreeMap = new TreeMap<>(); + } + + public void write(PopConsumerRecord record) { + lock.lock(); + try { + recordTreeMap.put(record.getOffset(), record); + } finally { + lock.unlock(); + } + } + + public boolean delete(PopConsumerRecord record) { + PopConsumerRecord popConsumerRecord; + lock.lock(); + try { + popConsumerRecord = recordTreeMap.remove(record.getOffset()); + } finally { + lock.unlock(); + } + return popConsumerRecord != null; + } + + public long getMinOffsetInBuffer() { + Map.Entry entry = recordTreeMap.firstEntry(); + return entry != null ? entry.getKey() : OFFSET_NOT_EXIST; + } + + public int getInFlightRecordCount() { + return recordTreeMap.size(); + } + + public List removeExpiredRecords(long currentTime) { + List result = null; + lock.lock(); + try { + Iterator> iterator = recordTreeMap.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + // org.apache.rocketmq.broker.processor.PopBufferMergeService.scan + if (entry.getValue().getVisibilityTimeout() <= currentTime || + entry.getValue().getPopTime() + brokerConfig.getPopCkStayBufferTime() <= currentTime) { + if (result == null) { + result = new ArrayList<>(); + } + result.add(entry.getValue()); + iterator.remove(); + } + } + } finally { + lock.unlock(); + } + return result; + } + + public String getGroupId() { + return groupId; + } + + public String getTopicId() { + return topicId; + } + + public int getQueueId() { + return queueId; + } + + @Override + public String toString() { + return "ConsumerRecords{" + + "lock=" + lock + + ", topicId=" + topicId + + ", groupId=" + groupId + + ", queueId=" + queueId + + ", recordTreeMap=" + recordTreeMap.size() + + '}'; + } + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerContext.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerContext.java new file mode 100644 index 0000000..0ad8bac --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerContext.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.broker.pop; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.remoting.protocol.header.ExtraInfoUtil; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.GetMessageStatus; + +public class PopConsumerContext { + + private final String clientHost; + + private final long popTime; + + private final long invisibleTime; + + private final String groupId; + + private final boolean fifo; + + private final int initMode; + + private final String attemptId; + + private final AtomicLong restCount; + + private final StringBuilder startOffsetInfo; + + private final StringBuilder msgOffsetInfo; + + private final StringBuilder orderCountInfo; + + private List getMessageResultList; + + private List popConsumerRecordList; + + public PopConsumerContext(String clientHost, + long popTime, long invisibleTime, String groupId, boolean fifo, int initMode, String attemptId) { + + this.clientHost = clientHost; + this.popTime = popTime; + this.invisibleTime = invisibleTime; + this.groupId = groupId; + this.fifo = fifo; + this.initMode = initMode; + this.attemptId = attemptId; + this.restCount = new AtomicLong(0); + this.startOffsetInfo = new StringBuilder(); + this.msgOffsetInfo = new StringBuilder(); + this.orderCountInfo = new StringBuilder(); + } + + public boolean isFound() { + return getMessageResultList != null && !getMessageResultList.isEmpty(); + } + + // offset is consumer last request offset + public void addGetMessageResult(GetMessageResult result, + String topicId, int queueId, PopConsumerRecord.RetryType retryType, long offset) { + + if (result.getStatus() != GetMessageStatus.FOUND || result.getMessageQueueOffset().isEmpty()) { + return; + } + + if (this.getMessageResultList == null) { + this.getMessageResultList = new ArrayList<>(); + } + + if (this.popConsumerRecordList == null) { + this.popConsumerRecordList = new ArrayList<>(); + } + + this.getMessageResultList.add(result); + this.addRestCount(result.getMaxOffset() - result.getNextBeginOffset()); + + for (int i = 0; i < result.getMessageQueueOffset().size(); i++) { + this.popConsumerRecordList.add(new PopConsumerRecord(popTime, groupId, topicId, queueId, + retryType.getCode(), invisibleTime, result.getMessageQueueOffset().get(i), attemptId)); + } + + ExtraInfoUtil.buildStartOffsetInfo(startOffsetInfo, topicId, queueId, offset); + ExtraInfoUtil.buildMsgOffsetInfo(msgOffsetInfo, topicId, queueId, result.getMessageQueueOffset()); + } + + public String getClientHost() { + return clientHost; + } + + public String getGroupId() { + return groupId; + } + + public void addRestCount(long delta) { + this.restCount.addAndGet(delta); + } + + public long getRestCount() { + return restCount.get(); + } + + public long getPopTime() { + return popTime; + } + + public boolean isFifo() { + return fifo; + } + + public int getInitMode() { + return initMode; + } + + public long getInvisibleTime() { + return invisibleTime; + } + + public String getAttemptId() { + return attemptId; + } + + public int getMessageCount() { + return getMessageResultList != null ? + getMessageResultList.stream().mapToInt(GetMessageResult::getMessageCount).sum() : 0; + } + + public String getStartOffsetInfo() { + return startOffsetInfo.toString(); + } + + public String getMsgOffsetInfo() { + return msgOffsetInfo.toString(); + } + + public StringBuilder getOrderCountInfoBuilder() { + return orderCountInfo; + } + + public String getOrderCountInfo() { + return orderCountInfo.toString(); + } + + public List getGetMessageResultList() { + return getMessageResultList; + } + + public List getPopConsumerRecordList() { + return popConsumerRecordList; + } + + @Override + public String toString() { + return "PopConsumerContext{" + + "clientHost=" + clientHost + + ", popTime=" + popTime + + ", invisibleTime=" + invisibleTime + + ", groupId=" + groupId + + ", isFifo=" + fifo + + ", attemptId=" + attemptId + + ", restCount=" + restCount + + ", startOffsetInfo=" + startOffsetInfo + + ", msgOffsetInfo=" + msgOffsetInfo + + ", orderCountInfo=" + orderCountInfo + + ", getMessageResultList=" + (getMessageResultList != null ? getMessageResultList.size() : 0) + + ", popConsumerRecordList=" + (popConsumerRecordList != null ? popConsumerRecordList.size() : 0) + + '}'; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerKVStore.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerKVStore.java new file mode 100644 index 0000000..33072d6 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerKVStore.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.broker.pop; + +import java.util.List; + +public interface PopConsumerKVStore { + + /** + * Starts the storage service. + */ + boolean start(); + + /** + * Shutdown the storage service. + */ + boolean shutdown(); + + /** + * Gets the file path of the storage. + * @return The file path of the storage. + */ + String getFilePath(); + + /** + * Writes a list of consumer records to the storage. + * @param consumerRecordList The list of consumer records to be written. + */ + void writeRecords(List consumerRecordList); + + /** + * Deletes a list of consumer records from the storage. + * @param consumerRecordList The list of consumer records to be deleted. + */ + void deleteRecords(List consumerRecordList); + + /** + * Scans and returns a list of expired consumer records within the specified time range. + * @param lowerTime The start time (inclusive) of the time range to search, in milliseconds. + * @param upperTime The end time (exclusive) of the time range to search, in milliseconds. + * @param maxCount The maximum number of records to return. + * Even if more records match the criteria, only this many will be returned. + * @return A list of expired consumer records within the specified time range. + * If no matching records are found, an empty list is returned. + */ + List scanExpiredRecords(long lowerTime, long upperTime, int maxCount); +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerLockService.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerLockService.java new file mode 100644 index 0000000..3322143 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerLockService.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.broker.pop; + +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.common.PopAckConstants; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.ConcurrentHashMapUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PopConsumerLockService { + + private static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); + + private final long timeout; + private final ConcurrentMap lockTable; + + public PopConsumerLockService(long timeout) { + this.timeout = timeout; + this.lockTable = new ConcurrentHashMap<>(); + } + + public boolean tryLock(String groupId, String topicId) { + return Objects.requireNonNull(ConcurrentHashMapUtils.computeIfAbsent(lockTable, + groupId + PopAckConstants.SPLIT + topicId, s -> new TimedLock())).tryLock(); + } + + public void unlock(String groupId, String topicId) { + TimedLock lock = lockTable.get(groupId + PopAckConstants.SPLIT + topicId); + if (lock != null) { + lock.unlock(); + } + } + + // For retry topics, should lock origin group and topic + public boolean isLockTimeout(String groupId, String topicId) { + topicId = KeyBuilder.parseNormalTopic(topicId, groupId); + TimedLock lock = lockTable.get(groupId + PopAckConstants.SPLIT + topicId); + return lock == null || System.currentTimeMillis() - lock.getLockTime() > timeout; + } + + public void removeTimeout() { + Iterator> iterator = lockTable.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (System.currentTimeMillis() - entry.getValue().getLockTime() > timeout) { + log.info("PopConsumerLockService remove timeout lock, " + + "key={}, locked={}", entry.getKey(), entry.getValue().lock.get()); + iterator.remove(); + } + } + } + + static class TimedLock { + private volatile long lockTime; + private final AtomicBoolean lock; + + public TimedLock() { + this.lockTime = System.currentTimeMillis(); + this.lock = new AtomicBoolean(false); + } + + public boolean tryLock() { + if (lock.compareAndSet(false, true)) { + this.lockTime = System.currentTimeMillis(); + return true; + } + return false; + } + + public void unlock() { + lock.set(false); + } + + public long getLockTime() { + return lockTime; + } + } +} \ No newline at end of file diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRecord.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRecord.java new file mode 100644 index 0000000..1ee01fe --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRecord.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.pop; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.annotation.JSONField; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +public class PopConsumerRecord { + + public enum RetryType { + + NORMAL_TOPIC(0), + + RETRY_TOPIC_V1(1), + + RETRY_TOPIC_V2(2); + + private final int code; + + RetryType(int code) { + this.code = code; + } + + public int getCode() { + return code; + } + } + + @JSONField() + private long popTime; + + @JSONField(ordinal = 1) + private String groupId; + + @JSONField(ordinal = 2) + private String topicId; + + @JSONField(ordinal = 3) + private int queueId; + + @JSONField(ordinal = 4) + private int retryFlag; + + @JSONField(ordinal = 5) + private long invisibleTime; + + @JSONField(ordinal = 6) + private long offset; + + @JSONField(ordinal = 7) + private int attemptTimes; + + @JSONField(ordinal = 8) + private String attemptId; + + // used for test and fastjson + public PopConsumerRecord() { + } + + public PopConsumerRecord(long popTime, String groupId, String topicId, int queueId, + int retryFlag, long invisibleTime, long offset, String attemptId) { + + this.popTime = popTime; + this.groupId = groupId; + this.topicId = topicId; + this.queueId = queueId; + this.retryFlag = retryFlag; + this.invisibleTime = invisibleTime; + this.offset = offset; + this.attemptId = attemptId; + } + + @JSONField(serialize = false) + public long getVisibilityTimeout() { + return popTime + invisibleTime; + } + + /** + * Key: timestamp(8) + groupId + topicId + queueId + offset + */ + @JSONField(serialize = false) + public byte[] getKeyBytes() { + int length = Long.BYTES + groupId.length() + 1 + topicId.length() + 1 + Integer.BYTES + 1 + Long.BYTES; + byte[] bytes = new byte[length]; + ByteBuffer buffer = ByteBuffer.wrap(bytes); + buffer.putLong(this.getVisibilityTimeout()); + buffer.put(groupId.getBytes(StandardCharsets.UTF_8)).put((byte) '@'); + buffer.put(topicId.getBytes(StandardCharsets.UTF_8)).put((byte) '@'); + buffer.putInt(queueId).put((byte) '@'); + buffer.putLong(offset); + return bytes; + } + + @JSONField(serialize = false) + public boolean isRetry() { + return retryFlag != 0; + } + + @JSONField(serialize = false) + public byte[] getValueBytes() { + return JSON.toJSONBytes(this); + } + + public static PopConsumerRecord decode(byte[] body) { + return JSONObject.parseObject(body, PopConsumerRecord.class); + } + + public long getPopTime() { + return popTime; + } + + public void setPopTime(long popTime) { + this.popTime = popTime; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public String getTopicId() { + return topicId; + } + + public void setTopicId(String topicId) { + this.topicId = topicId; + } + + public int getQueueId() { + return queueId; + } + + public void setQueueId(int queueId) { + this.queueId = queueId; + } + + public int getRetryFlag() { + return retryFlag; + } + + public void setRetryFlag(int retryFlag) { + this.retryFlag = retryFlag; + } + + public long getInvisibleTime() { + return invisibleTime; + } + + public void setInvisibleTime(long invisibleTime) { + this.invisibleTime = invisibleTime; + } + + public long getOffset() { + return offset; + } + + public void setOffset(long offset) { + this.offset = offset; + } + + public int getAttemptTimes() { + return attemptTimes; + } + + public void setAttemptTimes(int attemptTimes) { + this.attemptTimes = attemptTimes; + } + + public String getAttemptId() { + return attemptId; + } + + public void setAttemptId(String attemptId) { + this.attemptId = attemptId; + } + + @Override + public String toString() { + return "PopDeliveryRecord{" + + "popTime=" + popTime + + ", groupId='" + groupId + '\'' + + ", topicId='" + topicId + '\'' + + ", queueId=" + queueId + + ", retryFlag=" + retryFlag + + ", invisibleTime=" + invisibleTime + + ", offset=" + offset + + ", attemptTimes=" + attemptTimes + + ", attemptId='" + attemptId + '\'' + + '}'; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java new file mode 100644 index 0000000..7ab276a --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.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.broker.pop; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +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.constant.LoggerName; +import org.apache.rocketmq.store.rocksdb.RocksDBOptionsFactory; +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.Slice; +import org.rocksdb.WriteBatch; +import org.rocksdb.WriteOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PopConsumerRocksdbStore extends AbstractRocksDBStorage implements PopConsumerKVStore { + + private static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); + private static final byte[] COLUMN_FAMILY_NAME = "popState".getBytes(StandardCharsets.UTF_8); + + private WriteOptions writeOptions; + private WriteOptions deleteOptions; + protected ColumnFamilyHandle columnFamilyHandle; + + public PopConsumerRocksdbStore(String filePath) { + super(filePath); + } + + // https://www.cnblogs.com/renjc/p/rocksdb-class-db.html + // https://github.com/johnzeng/rocksdb-doc-cn/blob/master/doc/RocksDB-Tuning-Guide.md + protected void initOptions() { + this.options = RocksDBOptionsFactory.createDBOptions(); + + this.writeOptions = new WriteOptions(); + this.writeOptions.setSync(true); + this.writeOptions.setDisableWAL(false); + this.writeOptions.setNoSlowdown(false); + + this.deleteOptions = new WriteOptions(); + this.deleteOptions.setSync(true); + this.deleteOptions.setDisableWAL(false); + this.deleteOptions.setNoSlowdown(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(); + + // init column family here + ColumnFamilyOptions defaultOptions = RocksDBOptionsFactory.createPopCFOptions(); + ColumnFamilyOptions popStateOptions = RocksDBOptionsFactory.createPopCFOptions(); + this.cfOptions.add(defaultOptions); + this.cfOptions.add(popStateOptions); + + List cfDescriptors = new ArrayList<>(); + cfDescriptors.add(new ColumnFamilyDescriptor(RocksDB.DEFAULT_COLUMN_FAMILY, defaultOptions)); + cfDescriptors.add(new ColumnFamilyDescriptor(COLUMN_FAMILY_NAME, popStateOptions)); + this.open(cfDescriptors); + this.defaultCFHandle = cfHandles.get(0); + this.columnFamilyHandle = cfHandles.get(1); + + log.debug("PopConsumerRocksdbStore init, filePath={}", this.dbPath); + } catch (final Exception e) { + log.error("PopConsumerRocksdbStore init error, filePath={}", this.dbPath, e); + return false; + } + return true; + } + + public String getFilePath() { + return this.dbPath; + } + + @Override + public void writeRecords(List consumerRecordList) { + if (!consumerRecordList.isEmpty()) { + try (WriteBatch writeBatch = new WriteBatch()) { + for (PopConsumerRecord record : consumerRecordList) { + writeBatch.put(columnFamilyHandle, record.getKeyBytes(), record.getValueBytes()); + } + this.db.write(writeOptions, writeBatch); + } catch (RocksDBException e) { + throw new RuntimeException("Write record error", e); + } + } + } + + @Override + public void deleteRecords(List consumerRecordList) { + if (!consumerRecordList.isEmpty()) { + try (WriteBatch writeBatch = new WriteBatch()) { + for (PopConsumerRecord record : consumerRecordList) { + writeBatch.delete(columnFamilyHandle, record.getKeyBytes()); + } + this.db.write(deleteOptions, writeBatch); + } catch (RocksDBException e) { + throw new RuntimeException("Delete record error", e); + } + } + } + + @Override + // https://github.com/facebook/rocksdb/issues/10300 + public List scanExpiredRecords(long lower, long upper, int maxCount) { + // In RocksDB, we can use SstPartitionerFixedPrefixFactory in cfOptions + // and new ColumnFamilyOptions().useFixedLengthPrefixExtractor() to + // configure prefix indexing to improve the performance of scans. + // However, in the current implementation, this is not the bottleneck. + List consumerRecordList = new ArrayList<>(); + try (ReadOptions scanOptions = new ReadOptions() + .setIterateLowerBound(new Slice(ByteBuffer.allocate(Long.BYTES).putLong(lower).array())) + .setIterateUpperBound(new Slice(ByteBuffer.allocate(Long.BYTES).putLong(upper).array())); + RocksIterator iterator = db.newIterator(this.columnFamilyHandle, scanOptions)) { + iterator.seek(ByteBuffer.allocate(Long.BYTES).putLong(lower).array()); + while (iterator.isValid() && consumerRecordList.size() < maxCount) { + consumerRecordList.add(PopConsumerRecord.decode(iterator.value())); + iterator.next(); + } + } + return consumerRecordList; + } + + @Override + protected void preShutdown() { + if (this.writeOptions != null) { + this.writeOptions.close(); + } + if (this.deleteOptions != null) { + this.deleteOptions.close(); + } + if (this.defaultCFHandle != null) { + this.defaultCFHandle.close(); + } + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java new file mode 100644 index 0000000..1138ff4 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java @@ -0,0 +1,747 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.pop; + +import com.alibaba.fastjson.JSON; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Stopwatch; +import java.nio.ByteBuffer; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Triple; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.TopicFilterType; +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.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.utils.ConcurrentHashMapUtils; +import org.apache.rocketmq.remoting.protocol.header.ExtraInfoUtil; +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.PutMessageResult; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.exception.ConsumeQueueException; +import org.apache.rocketmq.store.pop.PopCheckPoint; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PopConsumerService extends ServiceThread { + + private static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); + private static final long OFFSET_NOT_EXIST = -1L; + private static final String ROCKSDB_DIRECTORY = "kvStore"; + private static final int[] REWRITE_INTERVALS_IN_SECONDS = + new int[] {10, 30, 60, 120, 180, 240, 300, 360, 420, 480, 540, 600, 1200, 1800, 3600, 7200}; + + private final AtomicBoolean consumerRunning; + private final BrokerConfig brokerConfig; + private final BrokerController brokerController; + private final AtomicLong currentTime; + private final AtomicLong lastCleanupLockTime; + private final PopConsumerCache popConsumerCache; + private final PopConsumerKVStore popConsumerStore; + private final PopConsumerLockService consumerLockService; + private final ConcurrentMap requestCountTable; + + public PopConsumerService(BrokerController brokerController) { + + this.brokerController = brokerController; + this.brokerConfig = brokerController.getBrokerConfig(); + + this.consumerRunning = new AtomicBoolean(false); + this.requestCountTable = new ConcurrentHashMap<>(); + this.currentTime = new AtomicLong(TimeUnit.SECONDS.toMillis(3)); + this.lastCleanupLockTime = new AtomicLong(System.currentTimeMillis()); + this.consumerLockService = new PopConsumerLockService(TimeUnit.MINUTES.toMillis(2)); + this.popConsumerStore = new PopConsumerRocksdbStore(Paths.get( + brokerController.getMessageStoreConfig().getStorePathRootDir(), ROCKSDB_DIRECTORY).toString()); + this.popConsumerCache = brokerConfig.isEnablePopBufferMerge() ? new PopConsumerCache( + brokerController, this.popConsumerStore, this.consumerLockService, this::revive) : null; + + log.info("PopConsumerService init, buffer={}, rocksdb filePath={}", + brokerConfig.isEnablePopBufferMerge(), this.popConsumerStore.getFilePath()); + } + + /** + * In-flight messages are those that have been received from a queue + * by a consumer but have not yet been deleted. For standard queues, + * there is a limit on the number of in-flight messages, depending on queue traffic and message backlog. + */ + public boolean isPopShouldStop(String group, String topic, int queueId) { + return brokerConfig.isEnablePopMessageThreshold() && popConsumerCache != null && + popConsumerCache.getPopInFlightMessageCount(group, topic, queueId) >= + brokerConfig.getPopInflightMessageThreshold(); + } + + public long getPendingFilterCount(String groupId, String topicId, int queueId) { + try { + long maxOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topicId, queueId); + long consumeOffset = this.brokerController.getConsumerOffsetManager().queryOffset(groupId, topicId, queueId); + return maxOffset - consumeOffset; + } catch (ConsumeQueueException e) { + throw new RuntimeException(e); + } + } + + public GetMessageResult recodeRetryMessage(GetMessageResult getMessageResult, + String topicId, long offset, long popTime, long invisibleTime) { + + if (getMessageResult.getMessageCount() == 0 || + getMessageResult.getMessageMapedList().isEmpty()) { + return getMessageResult; + } + + GetMessageResult result = new GetMessageResult(getMessageResult.getMessageCount()); + result.setStatus(GetMessageStatus.FOUND); + String brokerName = brokerConfig.getBrokerName(); + + for (SelectMappedBufferResult bufferResult : getMessageResult.getMessageMapedList()) { + List messageExtList = MessageDecoder.decodesBatch( + bufferResult.getByteBuffer(), true, false, true); + bufferResult.release(); + for (MessageExt messageExt : messageExtList) { + try { + // When override retry message topic to origin topic, + // need clear message store size to recode + String ckInfo = ExtraInfoUtil.buildExtraInfo(offset, popTime, invisibleTime, 0, + messageExt.getTopic(), brokerName, messageExt.getQueueId(), messageExt.getQueueOffset()); + messageExt.getProperties().putIfAbsent(MessageConst.PROPERTY_POP_CK, ckInfo); + messageExt.setTopic(topicId); + messageExt.setStoreSize(0); + byte[] encode = MessageDecoder.encode(messageExt, false); + ByteBuffer buffer = ByteBuffer.wrap(encode); + SelectMappedBufferResult tmpResult = new SelectMappedBufferResult( + bufferResult.getStartOffset(), buffer, encode.length, null); + result.addMessage(tmpResult); + } catch (Exception e) { + log.error("PopConsumerService exception in recode retry message, topic={}", topicId, e); + } + } + } + + return result; + } + + public PopConsumerContext handleGetMessageResult(PopConsumerContext context, GetMessageResult result, + String topicId, int queueId, PopConsumerRecord.RetryType retryType, long offset) { + + if (GetMessageStatus.FOUND.equals(result.getStatus()) && !result.getMessageQueueOffset().isEmpty()) { + if (context.isFifo()) { + this.setFifoBlocked(context, context.getGroupId(), topicId, queueId, result.getMessageQueueOffset()); + } + // build response header here + context.addGetMessageResult(result, topicId, queueId, retryType, offset); + if (brokerConfig.isPopConsumerKVServiceLog()) { + log.info("PopConsumerService pop, time={}, invisible={}, " + + "groupId={}, topic={}, queueId={}, offset={}, attemptId={}", + context.getPopTime(), context.getInvisibleTime(), context.getGroupId(), + topicId, queueId, result.getMessageQueueOffset(), context.getAttemptId()); + } + } + + long commitOffset = offset; + if (context.isFifo()) { + if (!GetMessageStatus.FOUND.equals(result.getStatus())) { + commitOffset = result.getNextBeginOffset(); + } + } else { + this.brokerController.getConsumerOffsetManager().commitPullOffset( + context.getClientHost(), context.getGroupId(), topicId, queueId, result.getNextBeginOffset()); + if (brokerConfig.isEnablePopBufferMerge() && popConsumerCache != null) { + long minOffset = popConsumerCache.getMinOffsetInCache(context.getGroupId(), topicId, queueId); + if (minOffset != OFFSET_NOT_EXIST) { + commitOffset = minOffset; + } + } + } + this.brokerController.getConsumerOffsetManager().commitOffset( + context.getClientHost(), context.getGroupId(), topicId, queueId, commitOffset); + return context; + } + + public long getPopOffset(String groupId, String topicId, int queueId, int initMode) { + long offset = this.brokerController.getConsumerOffsetManager().queryPullOffset(groupId, topicId, queueId); + if (offset < 0L) { + try { + offset = this.brokerController.getPopMessageProcessor() + .getInitOffset(topicId, groupId, queueId, initMode, true); + log.info("PopConsumerService init offset, groupId={}, topicId={}, queueId={}, init={}, offset={}", + groupId, topicId, queueId, ConsumeInitMode.MIN == initMode ? "min" : "max", offset); + } catch (ConsumeQueueException e) { + throw new RuntimeException(e); + } + } + Long resetOffset = + this.brokerController.getConsumerOffsetManager().queryThenEraseResetOffset(topicId, groupId, queueId); + if (resetOffset != null) { + this.clearCache(groupId, topicId, queueId); + this.brokerController.getConsumerOrderInfoManager().clearBlock(topicId, groupId, queueId); + this.brokerController.getConsumerOffsetManager() + .commitOffset("ResetPopOffset", groupId, topicId, queueId, resetOffset); + } + return resetOffset != null ? resetOffset : offset; + } + + public CompletableFuture getMessageAsync(String clientHost, + String groupId, String topicId, int queueId, long offset, int batchSize, MessageFilter filter) { + + log.debug("PopConsumerService getMessageAsync, groupId={}, topicId={}, queueId={}, offset={}, batchSize={}, filter={}", + groupId, topicId, offset, queueId, batchSize, filter != null); + + CompletableFuture getMessageFuture = + brokerController.getMessageStore().getMessageAsync(groupId, topicId, queueId, offset, batchSize, filter); + + // refer org.apache.rocketmq.broker.processor.PopMessageProcessor#popMsgFromQueue + return getMessageFuture.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. + this.brokerController.getConsumerOffsetManager().commitOffset( + clientHost, groupId, topicId, queueId, result.getNextBeginOffset()); + + log.warn("PopConsumerService getMessageAsync, initial offset because store is no correct, " + + "groupId={}, topicId={}, queueId={}, batchSize={}, offset={}->{}", + groupId, topicId, queueId, batchSize, offset, result.getNextBeginOffset()); + + return brokerController.getMessageStore().getMessageAsync( + groupId, topicId, queueId, result.getNextBeginOffset(), batchSize, filter); + } + + return CompletableFuture.completedFuture(result); + + }).whenComplete((result, throwable) -> { + if (throwable != null) { + log.error("Pop getMessageAsync error", throwable); + } + }); + } + + /** + * Fifo message does not have retry feature in broker + */ + public void setFifoBlocked(PopConsumerContext context, + String groupId, String topicId, int queueId, List queueOffsetList) { + brokerController.getConsumerOrderInfoManager().update( + context.getAttemptId(), false, topicId, groupId, queueId, + context.getPopTime(), context.getInvisibleTime(), queueOffsetList, context.getOrderCountInfoBuilder()); + } + + public boolean isFifoBlocked(PopConsumerContext context, String groupId, String topicId, int queueId) { + return brokerController.getConsumerOrderInfoManager().checkBlock( + context.getAttemptId(), topicId, groupId, queueId, context.getInvisibleTime()); + } + + protected CompletableFuture getMessageAsync(CompletableFuture future, + String clientHost, String groupId, String topicId, int queueId, int batchSize, MessageFilter filter, + PopConsumerRecord.RetryType retryType) { + + return future.thenCompose(result -> { + + // pop request too much, should not add rest count here + if (isPopShouldStop(groupId, topicId, queueId)) { + return CompletableFuture.completedFuture(result); + } + + // Current requests would calculate the total number of messages + // waiting to be filtered for new message arrival notifications in + // the long-polling service, need disregarding the backlog in order + // consumption scenario. If rest message num including the blocked + // queue accumulation would lead to frequent unnecessary wake-ups + // of long-polling requests, resulting unnecessary CPU usage. + // When client ack message, long-polling request would be notifications + // by AckMessageProcessor.ackOrderly() and message will not be delayed. + if (result.isFifo() && isFifoBlocked(result, groupId, topicId, queueId)) { + // should not add accumulation(max offset - consumer offset) here + return CompletableFuture.completedFuture(result); + } + + int remain = batchSize - result.getMessageCount(); + if (remain <= 0) { + result.addRestCount(this.getPendingFilterCount(groupId, topicId, queueId)); + return CompletableFuture.completedFuture(result); + } else { + final long consumeOffset = this.getPopOffset(groupId, topicId, queueId, result.getInitMode()); + return getMessageAsync(clientHost, groupId, topicId, queueId, consumeOffset, remain, filter) + .thenApply(getMessageResult -> handleGetMessageResult( + result, getMessageResult, topicId, queueId, retryType, consumeOffset)); + } + }); + } + + public CompletableFuture popAsync(String clientHost, long popTime, long invisibleTime, + String groupId, String topicId, int queueId, int batchSize, boolean fifo, String attemptId, int initMode, + MessageFilter filter) { + + PopConsumerContext popConsumerContext = + new PopConsumerContext(clientHost, popTime, invisibleTime, groupId, fifo, initMode, attemptId); + + TopicConfig topicConfig = brokerController.getTopicConfigManager().selectTopicConfig(topicId); + if (topicConfig == null || !consumerLockService.tryLock(groupId, topicId)) { + return CompletableFuture.completedFuture(popConsumerContext); + } + + log.debug("PopConsumerService popAsync, groupId={}, topicId={}, queueId={}, " + + "batchSize={}, invisibleTime={}, fifo={}, attemptId={}, filter={}", + groupId, topicId, queueId, batchSize, invisibleTime, fifo, attemptId, filter); + + String requestKey = groupId + "@" + topicId; + String retryTopicV1 = KeyBuilder.buildPopRetryTopicV1(topicId, groupId); + String retryTopicV2 = KeyBuilder.buildPopRetryTopicV2(topicId, groupId); + long requestCount = Objects.requireNonNull(ConcurrentHashMapUtils.computeIfAbsent( + requestCountTable, requestKey, k -> new AtomicLong(0L))).getAndIncrement(); + boolean preferRetry = requestCount % 5L == 0L; + + CompletableFuture getMessageFuture = + CompletableFuture.completedFuture(popConsumerContext); + + try { + if (!fifo && preferRetry) { + if (brokerConfig.isRetrieveMessageFromPopRetryTopicV1()) { + getMessageFuture = this.getMessageAsync(getMessageFuture, clientHost, groupId, + retryTopicV1, 0, batchSize, filter, PopConsumerRecord.RetryType.RETRY_TOPIC_V1); + } + + if (brokerConfig.isEnableRetryTopicV2()) { + getMessageFuture = this.getMessageAsync(getMessageFuture, clientHost, groupId, + retryTopicV2, 0, batchSize, filter, PopConsumerRecord.RetryType.RETRY_TOPIC_V2); + } + } + + if (queueId != -1) { + getMessageFuture = this.getMessageAsync(getMessageFuture, clientHost, groupId, + topicId, queueId, batchSize, filter, PopConsumerRecord.RetryType.NORMAL_TOPIC); + } else { + for (int i = 0; i < topicConfig.getReadQueueNums(); i++) { + int current = (int) ((requestCount + i) % topicConfig.getReadQueueNums()); + getMessageFuture = this.getMessageAsync(getMessageFuture, clientHost, groupId, + topicId, current, batchSize, filter, PopConsumerRecord.RetryType.NORMAL_TOPIC); + } + + if (!fifo && !preferRetry) { + if (brokerConfig.isRetrieveMessageFromPopRetryTopicV1()) { + getMessageFuture = this.getMessageAsync(getMessageFuture, clientHost, groupId, + retryTopicV1, 0, batchSize, filter, PopConsumerRecord.RetryType.RETRY_TOPIC_V1); + } + + if (brokerConfig.isEnableRetryTopicV2()) { + getMessageFuture = this.getMessageAsync(getMessageFuture, clientHost, groupId, + retryTopicV2, 0, batchSize, filter, PopConsumerRecord.RetryType.RETRY_TOPIC_V2); + } + } + } + + return getMessageFuture.thenCompose(result -> { + if (result.isFound() && !result.isFifo()) { + if (brokerConfig.isEnablePopBufferMerge() && + popConsumerCache != null && !popConsumerCache.isCacheFull()) { + this.popConsumerCache.writeRecords(result.getPopConsumerRecordList()); + } else { + this.popConsumerStore.writeRecords(result.getPopConsumerRecordList()); + } + + for (int i = 0; i < result.getGetMessageResultList().size(); i++) { + GetMessageResult getMessageResult = result.getGetMessageResultList().get(i); + PopConsumerRecord popConsumerRecord = result.getPopConsumerRecordList().get(i); + + // If the buffer belong retries message, the message needs to be re-encoded. + // The buffer should not be re-encoded when popResponseReturnActualRetryTopic + // is true or the current topic is not a retry topic. + boolean recode = brokerConfig.isPopResponseReturnActualRetryTopic(); + if (recode && popConsumerRecord.isRetry()) { + result.getGetMessageResultList().set(i, this.recodeRetryMessage( + getMessageResult, popConsumerRecord.getTopicId(), + popConsumerRecord.getQueueId(), result.getPopTime(), invisibleTime)); + } + } + } + return CompletableFuture.completedFuture(result); + }).whenComplete((result, throwable) -> { + try { + if (throwable != null) { + log.error("PopConsumerService popAsync get message error", + throwable instanceof CompletionException ? throwable.getCause() : throwable); + } + if (result.getMessageCount() > 0) { + log.debug("PopConsumerService popAsync result, found={}, groupId={}, topicId={}, queueId={}, " + + "batchSize={}, invisibleTime={}, fifo={}, attemptId={}, filter={}", result.getMessageCount(), + groupId, topicId, queueId, batchSize, invisibleTime, fifo, attemptId, filter); + } + } finally { + consumerLockService.unlock(groupId, topicId); + } + }); + } catch (Throwable t) { + log.error("PopConsumerService popAsync error", t); + } + + return getMessageFuture; + } + + // Notify polling request when receive orderly ack + public CompletableFuture ackAsync( + long popTime, long invisibleTime, String groupId, String topicId, int queueId, long offset) { + + if (brokerConfig.isPopConsumerKVServiceLog()) { + log.info("PopConsumerService ack, time={}, invisible={}, groupId={}, topic={}, queueId={}, offset={}", + popTime, invisibleTime, groupId, topicId, queueId, offset); + } + + PopConsumerRecord record = new PopConsumerRecord( + popTime, groupId, topicId, queueId, 0, invisibleTime, offset, null); + + if (brokerConfig.isEnablePopBufferMerge() && popConsumerCache != null) { + if (popConsumerCache.deleteRecords(Collections.singletonList(record)).isEmpty()) { + return CompletableFuture.completedFuture(true); + } + } + + this.popConsumerStore.deleteRecords(Collections.singletonList(record)); + return CompletableFuture.completedFuture(true); + } + + // refer ChangeInvisibleTimeProcessor.appendCheckPointThenAckOrigin + public void changeInvisibilityDuration(long popTime, long invisibleTime, + long changedPopTime, long changedInvisibleTime, String groupId, String topicId, int queueId, long offset) { + + if (brokerConfig.isPopConsumerKVServiceLog()) { + log.info("PopConsumerService change, time={}, invisible={}, " + + "groupId={}, topic={}, queueId={}, offset={}, new time={}, new invisible={}", + popTime, invisibleTime, groupId, topicId, queueId, offset, changedPopTime, changedInvisibleTime); + } + + PopConsumerRecord ckRecord = new PopConsumerRecord( + changedPopTime, groupId, topicId, queueId, 0, changedInvisibleTime, offset, null); + + PopConsumerRecord ackRecord = new PopConsumerRecord( + popTime, groupId, topicId, queueId, 0, invisibleTime, offset, null); + + this.popConsumerStore.writeRecords(Collections.singletonList(ckRecord)); + + if (brokerConfig.isEnablePopBufferMerge() && popConsumerCache != null) { + if (popConsumerCache.deleteRecords(Collections.singletonList(ackRecord)).isEmpty()) { + return; + } + } + + this.popConsumerStore.deleteRecords(Collections.singletonList(ackRecord)); + } + + // Use broker escape bridge to support remote read + public CompletableFuture> getMessageAsync(PopConsumerRecord consumerRecord) { + return this.brokerController.getEscapeBridge().getMessageAsync(consumerRecord.getTopicId(), + consumerRecord.getOffset(), consumerRecord.getQueueId(), brokerConfig.getBrokerName(), false); + } + + public CompletableFuture revive(PopConsumerRecord record) { + return this.getMessageAsync(record) + .thenCompose(result -> { + if (result == null) { + log.error("PopConsumerService revive error, message may be lost, record={}", record); + return CompletableFuture.completedFuture(false); + } + // true in triple right means get message needs to be retried + if (result.getLeft() == null) { + log.info("PopConsumerService revive no need retry, record={}", record); + return CompletableFuture.completedFuture(!result.getRight()); + } + return CompletableFuture.completedFuture(this.reviveRetry(record, result.getLeft())); + }); + } + + public void clearCache(String groupId, String topicId, int queueId) { + while (consumerLockService.tryLock(groupId, topicId)) { + } + try { + if (popConsumerCache != null) { + popConsumerCache.removeRecords(groupId, topicId, queueId); + } + } finally { + consumerLockService.unlock(groupId, topicId); + } + } + + public long revive(AtomicLong currentTime, int maxCount) { + Stopwatch stopwatch = Stopwatch.createStarted(); + long upperTime = System.currentTimeMillis() - 50L; + List consumerRecords = this.popConsumerStore.scanExpiredRecords( + currentTime.get() - TimeUnit.SECONDS.toMillis(3), upperTime, maxCount); + long scanCostTime = stopwatch.elapsed(TimeUnit.MILLISECONDS); + Queue failureList = new LinkedBlockingQueue<>(); + List> futureList = new ArrayList<>(consumerRecords.size()); + + // could merge read operation here + for (PopConsumerRecord record : consumerRecords) { + futureList.add(this.revive(record).thenAccept(result -> { + if (!result) { + if (record.getAttemptTimes() < brokerConfig.getPopReviveMaxAttemptTimes()) { + long backoffInterval = 1000L * REWRITE_INTERVALS_IN_SECONDS[ + Math.min(REWRITE_INTERVALS_IN_SECONDS.length, record.getAttemptTimes())]; + long nextInvisibleTime = record.getInvisibleTime() + backoffInterval; + PopConsumerRecord retryRecord = new PopConsumerRecord(System.currentTimeMillis(), + record.getGroupId(), record.getTopicId(), record.getQueueId(), + record.getRetryFlag(), nextInvisibleTime, record.getOffset(), record.getAttemptId()); + retryRecord.setAttemptTimes(record.getAttemptTimes() + 1); + failureList.add(retryRecord); + log.warn("PopConsumerService revive backoff retry, record={}", retryRecord); + } else { + log.error("PopConsumerService drop record, message may be lost, record={}", record); + } + } + })); + } + + CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).join(); + this.popConsumerStore.writeRecords(new ArrayList<>(failureList)); + this.popConsumerStore.deleteRecords(consumerRecords); + currentTime.set(consumerRecords.isEmpty() ? + upperTime : consumerRecords.get(consumerRecords.size() - 1).getVisibilityTimeout()); + + if (brokerConfig.isEnablePopBufferMerge()) { + log.info("PopConsumerService, key size={}, cache size={}, revive count={}, failure count={}, " + + "behindInMillis={}, scanInMillis={}, costInMillis={}", + popConsumerCache.getCacheKeySize(), popConsumerCache.getCacheSize(), + consumerRecords.size(), failureList.size(), upperTime - currentTime.get(), + scanCostTime, stopwatch.elapsed(TimeUnit.MILLISECONDS)); + } else { + log.info("PopConsumerService, revive count={}, failure count={}, " + + "behindInMillis={}, scanInMillis={}, costInMillis={}", + consumerRecords.size(), failureList.size(), upperTime - currentTime.get(), + scanCostTime, stopwatch.elapsed(TimeUnit.MILLISECONDS)); + } + + return consumerRecords.size(); + } + + public void createRetryTopicIfNeeded(String groupId, String topicId) { + TopicConfig topicConfig = brokerController.getTopicConfigManager().selectTopicConfig(topicId); + if (topicConfig != null) { + return; + } + + topicConfig = new TopicConfig(topicId, 1, 1, + PermName.PERM_READ | PermName.PERM_WRITE, 0); + topicConfig.setTopicFilterType(TopicFilterType.SINGLE_TAG); + brokerController.getTopicConfigManager().updateTopicConfig(topicConfig); + + long offset = this.brokerController.getConsumerOffsetManager().queryOffset(groupId, topicId, 0); + if (offset < 0) { + this.brokerController.getConsumerOffsetManager().commitOffset( + "InitPopOffset", groupId, topicId, 0, 0); + } + } + + @SuppressWarnings("DuplicatedCode") + // org.apache.rocketmq.broker.processor.PopReviveService#reviveRetry + public boolean reviveRetry(PopConsumerRecord record, MessageExt messageExt) { + + if (brokerConfig.isPopConsumerKVServiceLog()) { + log.info("PopConsumerService revive, time={}, invisible={}, groupId={}, topic={}, queueId={}, offset={}", + record.getPopTime(), record.getInvisibleTime(), record.getGroupId(), record.getTopicId(), + record.getQueueId(), record.getOffset()); + } + + boolean retry = StringUtils.startsWith(record.getTopicId(), MixAll.RETRY_GROUP_TOPIC_PREFIX); + String retryTopic = retry ? record.getTopicId() : KeyBuilder.buildPopRetryTopic( + record.getTopicId(), record.getGroupId(), brokerConfig.isEnableRetryTopicV2()); + this.createRetryTopicIfNeeded(record.getGroupId(), retryTopic); + + // deep copy here + MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + msgInner.setTopic(retryTopic); + msgInner.setBody(messageExt.getBody() != null ? messageExt.getBody() : new byte[] {}); + 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()); + + // set first pop time here + if (messageExt.getReconsumeTimes() == 0 || + msgInner.getProperties().get(MessageConst.PROPERTY_FIRST_POP_TIME) == null) { + msgInner.getProperties().put(MessageConst.PROPERTY_FIRST_POP_TIME, String.valueOf(record.getPopTime())); + } + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); + + PutMessageResult putMessageResult = + brokerController.getEscapeBridge().putMessageToSpecificQueue(msgInner); + + if (putMessageResult.getAppendMessageResult() == null || + putMessageResult.getAppendMessageResult().getStatus() != AppendMessageStatus.PUT_OK) { + log.error("PopConsumerService revive retry msg error, put status={}, ck={}, delay={}ms", + putMessageResult, JSON.toJSONString(record), System.currentTimeMillis() - record.getVisibilityTimeout()); + return false; + } + + if (this.brokerController.getBrokerStatsManager() != null) { + this.brokerController.getBrokerStatsManager().incBrokerPutNums(msgInner.getTopic(), 1); + this.brokerController.getBrokerStatsManager().incTopicPutNums(msgInner.getTopic()); + this.brokerController.getBrokerStatsManager().incTopicPutSize( + msgInner.getTopic(), putMessageResult.getAppendMessageResult().getWroteBytes()); + } + return true; + } + + // Export kv store record to revive topic + @SuppressWarnings("ExtractMethodRecommender") + public synchronized void transferToFsStore() { + Stopwatch stopwatch = Stopwatch.createStarted(); + while (true) { + try { + List consumerRecords = this.popConsumerStore.scanExpiredRecords( + 0, Long.MAX_VALUE, brokerConfig.getPopReviveMaxReturnSizePerRead()); + if (consumerRecords == null || consumerRecords.isEmpty()) { + break; + } + for (PopConsumerRecord record : consumerRecords) { + PopCheckPoint ck = new PopCheckPoint(); + ck.setBitMap(0); + ck.setNum((byte) 1); + ck.setPopTime(record.getPopTime()); + ck.setInvisibleTime(record.getInvisibleTime()); + ck.setStartOffset(record.getOffset()); + ck.setCId(record.getGroupId()); + ck.setTopic(record.getTopicId()); + ck.setQueueId(record.getQueueId()); + ck.setBrokerName(brokerConfig.getBrokerName()); + ck.addDiff(0); + ck.setRePutTimes(ck.getRePutTimes()); + int reviveQueueId = (int) record.getOffset() % brokerConfig.getReviveQueueNum(); + MessageExtBrokerInner ckMsg = + brokerController.getPopMessageProcessor().buildCkMsg(ck, reviveQueueId); + brokerController.getMessageStore().asyncPutMessage(ckMsg).join(); + } + log.info("PopConsumerStore transfer from kvStore to fsStore, count={}", consumerRecords.size()); + this.popConsumerStore.deleteRecords(consumerRecords); + this.waitForRunning(1); + } catch (Throwable t) { + log.error("PopConsumerStore transfer from kvStore to fsStore failure", t); + } + } + log.info("PopConsumerStore transfer to fsStore finish, cost={}ms", stopwatch.elapsed(TimeUnit.MILLISECONDS)); + } + + @Override + public String getServiceName() { + return PopConsumerService.class.getSimpleName(); + } + + @VisibleForTesting + protected PopConsumerKVStore getPopConsumerStore() { + return popConsumerStore; + } + + public PopConsumerLockService getConsumerLockService() { + return consumerLockService; + } + + @Override + public void start() { + if (!this.popConsumerStore.start()) { + throw new RuntimeException("PopConsumerStore init error"); + } + if (this.popConsumerCache != null) { + this.popConsumerCache.start(); + } + super.start(); + } + + @Override + public void shutdown() { + // Block shutdown thread until write records finish + super.shutdown(); + do { + this.waitForRunning(10); + } + while (consumerRunning.get()); + if (this.popConsumerCache != null) { + this.popConsumerCache.shutdown(); + } + if (this.popConsumerStore != null) { + this.popConsumerStore.shutdown(); + } + } + + @Override + public void run() { + this.consumerRunning.set(true); + while (!isStopped()) { + try { + // to prevent concurrency issues during read and write operations + long reviveCount = this.revive(this.currentTime, + brokerConfig.getPopReviveMaxReturnSizePerRead()); + + long current = System.currentTimeMillis(); + if (lastCleanupLockTime.get() + TimeUnit.MINUTES.toMillis(1) < current) { + this.consumerLockService.removeTimeout(); + this.lastCleanupLockTime.set(current); + } + + if (reviveCount < brokerConfig.getPopReviveMaxReturnSizePerRead()) { + this.waitForRunning(500); + } + } catch (Exception e) { + log.error("PopConsumerService revive error", e); + this.waitForRunning(500); + } + } + this.consumerRunning.set(false); + } +} 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 new file mode 100644 index 0000000..16c137d --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/AbstractSendMessageProcessor.java @@ -0,0 +1,596 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.opentelemetry.api.common.Attributes; +import java.net.SocketAddress; +import java.util.List; +import java.util.Map; +import java.util.Optional; +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; +import org.apache.rocketmq.common.help.FAQUrl; +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.MessageType; +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.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.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 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()); + private List sendMessageHookList; + + public AbstractSendMessageProcessor(final BrokerController brokerController) { + this.brokerController = brokerController; + } + + 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); + } + + 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()); + properties.put(MessageConst.PROPERTY_MSG_REGION, this.brokerController.getBrokerConfig().getRegionId()); + properties.put(MessageConst.PROPERTY_TRACE_SWITCH, String.valueOf(this.brokerController.getBrokerConfig().isTraceOn())); + requestHeader.setProperties(MessageDecoder.messageProperties2String(properties)); + + String uniqueKey = properties.get(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX); + sendMessageContext.setMsgUniqueKey(Optional.ofNullable(uniqueKey).orElse("")); + + if (properties.containsKey(MessageConst.PROPERTY_SHARDING_KEY)) { + sendMessageContext.setMsgType(MessageType.Order_Msg); + } else if (properties.containsKey(MessageConst.PROPERTY_DELAY_TIME_LEVEL) + || properties.containsKey(MessageConst.PROPERTY_TIMER_DELIVER_MS) + || properties.containsKey(MessageConst.PROPERTY_TIMER_DELAY_SEC) + || properties.containsKey(MessageConst.PROPERTY_TIMER_DELAY_MS)) { + sendMessageContext.setMsgType(MessageType.Delay_Msg); + } else if (Boolean.parseBoolean(properties.get(MessageConst.PROPERTY_TRANSACTION_PREPARED))) { + sendMessageContext.setMsgType(MessageType.Trans_Msg_Half); + } else { + sendMessageContext.setMsgType(MessageType.Normal_Msg); + } + return sendMessageContext; + } + + public boolean hasSendMessageHook() { + return sendMessageHookList != null && !this.sendMessageHookList.isEmpty(); + } + + protected MessageExtBrokerInner buildInnerMsg(final ChannelHandlerContext ctx, + final SendMessageRequestHeader requestHeader, final byte[] body, TopicConfig topicConfig) { + int queueIdInt = requestHeader.getQueueId(); + if (queueIdInt < 0) { + queueIdInt = randomQueueId(topicConfig.getWriteQueueNums()); + } + int sysFlag = requestHeader.getSysFlag(); + + if (TopicFilterType.MULTI_TAG == topicConfig.getTopicFilterType()) { + sysFlag |= MessageSysFlag.MULTI_TAGS_FLAG; + } + + MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + msgInner.setTopic(requestHeader.getTopic()); + msgInner.setBody(body); + msgInner.setFlag(requestHeader.getFlag()); + MessageAccessor.setProperties(msgInner, + MessageDecoder.string2messageProperties(requestHeader.getProperties())); + msgInner.setPropertiesString(requestHeader.getProperties()); + msgInner.setTagsCode(MessageExtBrokerInner.tagsString2tagsCode(topicConfig.getTopicFilterType(), + msgInner.getTags())); + + msgInner.setQueueId(queueIdInt); + msgInner.setSysFlag(sysFlag); + msgInner.setBornTimestamp(requestHeader.getBornTimestamp()); + msgInner.setBornHost(ctx.channel().remoteAddress()); + msgInner.setStoreHost(this.getStoreHost()); + msgInner.setReconsumeTimes(requestHeader.getReconsumeTimes() == null ? 0 : requestHeader + .getReconsumeTimes()); + return msgInner; + } + + public SocketAddress getStoreHost() { + return brokerController.getStoreHost(); + } + + protected RemotingCommand msgContentCheck(final ChannelHandlerContext ctx, + final SendMessageRequestHeader requestHeader, RemotingCommand request, + final RemotingCommand response) { + 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) { + 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) { + 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; + } + return response; + } + + protected RemotingCommand msgCheck(final ChannelHandlerContext ctx, + 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); + response.setRemark("the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1() + + "] sending message is forbidden"); + return response; + } + + TopicValidator.ValidateTopicResult result = TopicValidator.validateTopic(requestHeader.getTopic()); + if (!result.isValid()) { + response.setCode(ResponseCode.INVALID_PARAMETER); + 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; + } + + TopicConfig topicConfig = + this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic()); + if (null == topicConfig) { + int topicSysFlag = 0; + if (requestHeader.isUnitMode()) { + if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + topicSysFlag = TopicSysFlag.buildSysFlag(false, true); + } else { + topicSysFlag = TopicSysFlag.buildSysFlag(true, false); + } + } + + LOGGER.warn("the topic {} not exist, producer: {}", requestHeader.getTopic(), ctx.channel().remoteAddress()); + topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageMethod( + requestHeader.getTopic(), + requestHeader.getDefaultTopic(), + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), + requestHeader.getDefaultTopicQueueNums(), topicSysFlag); + + if (null == topicConfig) { + if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + topicConfig = + this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod( + requestHeader.getTopic(), 1, PermName.PERM_WRITE | PermName.PERM_READ, + topicSysFlag); + } + } + + if (null == topicConfig) { + response.setCode(ResponseCode.TOPIC_NOT_EXIST); + response.setRemark("topic[" + requestHeader.getTopic() + "] not exist, apply first please!" + + FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL)); + return response; + } + } + + int queueIdInt = requestHeader.getQueueId(); + int idValid = Math.max(topicConfig.getWriteQueueNums(), topicConfig.getReadQueueNums()); + if (queueIdInt >= idValid) { + String errorInfo = String.format("request queueId[%d] is illegal, %s Producer: %s", + queueIdInt, + topicConfig, + RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + + LOGGER.warn(errorInfo); + response.setCode(ResponseCode.INVALID_PARAMETER); + response.setRemark(errorInfo); + + return response; + } + return response; + } + + public void registerSendMessageHook(List sendMessageHookList) { + this.sendMessageHookList = sendMessageHookList; + } + + protected void doResponse(ChannelHandlerContext ctx, RemotingCommand request, + final RemotingCommand response) { + NettyRemotingAbstract.writeResponse(ctx.channel(), request, response); + } + + public void executeSendMessageHookBefore(SendMessageContext context) { + if (hasSendMessageHook()) { + for (SendMessageHook hook : this.sendMessageHookList) { + try { + hook.sendMessageBefore(context); + } catch (AbortProcessException e) { + throw e; + } catch (Throwable e) { + //ignore + } + } + } + } + + protected SendMessageRequestHeader parseRequestHeader(RemotingCommand request) throws RemotingCommandException { + return SendMessageRequestHeader.parseRequestHeader(request); + } + + protected int randomQueueId(int writeQueueNums) { + return ThreadLocalRandom.current().nextInt(99999999) % writeQueueNums; + } + + public void executeSendMessageHookAfter(final RemotingCommand response, final SendMessageContext context) { + if (hasSendMessageHook()) { + for (SendMessageHook hook : this.sendMessageHookList) { + try { + if (response != null) { + final SendMessageResponseHeader responseHeader = + (SendMessageResponseHeader) response.readCustomHeader(); + context.setMsgId(responseHeader.getMsgId()); + context.setQueueId(responseHeader.getQueueId()); + context.setQueueOffset(responseHeader.getQueueOffset()); + context.setCode(response.getCode()); + context.setErrorMsg(response.getRemark()); + } + hook.sendMessageAfter(context); + } catch (Throwable e) { + //ignore + } + } + } + } + + @Override + public boolean rejectRequest() { + return false; + } +} 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 0000000..23a4f61 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java @@ -0,0 +1,479 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 java.nio.charset.StandardCharsets; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.metrics.PopMetricsManager; +import org.apache.rocketmq.broker.offset.ConsumerOffsetManager; +import org.apache.rocketmq.broker.offset.ConsumerOrderInfoManager; +import org.apache.rocketmq.broker.pop.PopConsumerLockService; +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.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.exception.ConsumeQueueException; +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; + try { + maxOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(requestHeader.getTopic(), requestHeader.getQueueId()); + } catch (ConsumeQueueException e) { + throw new RemotingCommandException("Failed to get max offset", e); + } + 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; + } + if (brokerController.getBrokerConfig().isPopConsumerKVServiceEnable()) { + appendAckNew(requestHeader, null, response, channel, null); + } else { + 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()) { + if (brokerController.getBrokerConfig().isPopConsumerKVServiceEnable()) { + appendAckNew(null, bAck, response, channel, reqBody.getBrokerName()); + } else { + 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) throws RemotingCommandException { + 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; + try { + maxOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, qId); + } catch (ConsumeQueueException e) { + throw new RemotingCommandException("Failed to get max offset in queue", e); + } + 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(StandardCharsets.UTF_8)); + 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())); + if (brokerController.getBrokerConfig().isAppendAckAsync()) { + int finalAckCount = ackCount; + this.brokerController.getEscapeBridge().asyncPutMessageToSpecificQueue(msgInner).thenAccept(putMessageResult -> { + handlePutMessageResult(putMessageResult, ackMsg, topic, consumeGroup, popTime, qId, finalAckCount); + }).exceptionally(throwable -> { + handlePutMessageResult(new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, null, false), + ackMsg, topic, consumeGroup, popTime, qId, finalAckCount); + POP_LOGGER.error("put ack msg error ", throwable); + return null; + }); + } else { + PutMessageResult putMessageResult = this.brokerController.getEscapeBridge().putMessageToSpecificQueue(msgInner); + handlePutMessageResult(putMessageResult, ackMsg, topic, consumeGroup, popTime, qId, ackCount); + } + } + + private void appendAckNew(final AckMessageRequestHeader requestHeader, final BatchAck batchAck, + final RemotingCommand response, final Channel channel, String brokerName) throws RemotingCommandException { + + if (requestHeader != null && batchAck == null) { + String[] extraInfo = ExtraInfoUtil.split(requestHeader.getExtraInfo()); + String groupId = requestHeader.getConsumerGroup(); + String topicId = requestHeader.getTopic(); + int queueId = requestHeader.getQueueId(); + long ackOffset = requestHeader.getOffset(); + long popTime = ExtraInfoUtil.getPopTime(extraInfo); + long invisibleTime = ExtraInfoUtil.getInvisibleTime(extraInfo); + + int reviveQueueId = ExtraInfoUtil.getReviveQid(extraInfo); + if (reviveQueueId == KeyBuilder.POP_ORDER_REVIVE_QUEUE) { + ackOrderlyNew(topicId, groupId, queueId, ackOffset, popTime, invisibleTime, channel, response); + } else { + this.brokerController.getPopConsumerService().ackAsync( + popTime, invisibleTime, groupId, topicId, queueId, ackOffset); + } + + this.brokerController.getBrokerStatsManager().incBrokerAckNums(1); + this.brokerController.getBrokerStatsManager().incGroupAckNums(groupId, topicId, 1); + } else { + String groupId = batchAck.getConsumerGroup(); + String topicId = ExtraInfoUtil.getRealTopic( + batchAck.getTopic(), batchAck.getConsumerGroup(), batchAck.getRetry()); + int queueId = batchAck.getQueueId(); + int reviveQueueId = batchAck.getReviveQueueId(); + long startOffset = batchAck.getStartOffset(); + long popTime = batchAck.getPopTime(); + long invisibleTime = batchAck.getInvisibleTime(); + + try { + long minOffset = this.brokerController.getMessageStore().getMinOffsetInQueue(topicId, queueId); + long maxOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topicId, queueId); + if (minOffset == -1 || maxOffset == -1) { + POP_LOGGER.error("Illegal topic or queue found when batch ack {}", batchAck); + return; + } + + int ackCount = 0; + // Maintain consistency with the old implementation code style + 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 (reviveQueueId == KeyBuilder.POP_ORDER_REVIVE_QUEUE) { + ackOrderlyNew(topicId, groupId, queueId, offset, popTime, invisibleTime, channel, response); + } else { + this.brokerController.getPopConsumerService().ackAsync( + popTime, invisibleTime, groupId, topicId, queueId, offset); + } + ackCount++; + } + + this.brokerController.getBrokerStatsManager().incBrokerAckNums(ackCount); + this.brokerController.getBrokerStatsManager().incGroupAckNums(groupId, topicId, ackCount); + } catch (ConsumeQueueException e) { + throw new RemotingCommandException("Failed to ack message", e); + } + } + } + + private void handlePutMessageResult(PutMessageResult putMessageResult, AckMsg ackMsg, String topic, + String consumeGroup, long popTime, int qId, int ackCount) { + 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, qId, consumeGroup); + } + } 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); + } + + protected void ackOrderlyNew(String topic, String consumeGroup, int qId, long ackOffset, long popTime, + long invisibleTime, Channel channel, RemotingCommand response) { + + ConsumerOffsetManager consumerOffsetManager = this.brokerController.getConsumerOffsetManager(); + ConsumerOrderInfoManager consumerOrderInfoManager = brokerController.getConsumerOrderInfoManager(); + PopConsumerLockService consumerLockService = this.brokerController.getPopConsumerService().getConsumerLockService(); + + long oldOffset = consumerOffsetManager.queryOffset(consumeGroup, topic, qId); + if (ackOffset < oldOffset) { + return; + } + + while (!consumerLockService.tryLock(consumeGroup, topic)) { + } + + try { + // double check + oldOffset = consumerOffsetManager.queryOffset(consumeGroup, topic, qId); + if (ackOffset < oldOffset) { + return; + } + + long nextOffset = consumerOrderInfoManager.commitAndNext(topic, consumeGroup, qId, ackOffset, popTime); + if (brokerController.getBrokerConfig().isPopConsumerKVServiceLog()) { + POP_LOGGER.info("PopConsumerService ack orderly, time={}, topicId={}, groupId={}, queueId={}, " + + "offset={}, next={}", popTime, topic, consumeGroup, qId, ackOffset, nextOffset); + } + + if (nextOffset > -1L) { + if (!consumerOffsetManager.hasOffsetReset(topic, consumeGroup, qId)) { + String remoteAddress = RemotingHelper.parseSocketAddressAddr(channel.remoteAddress()); + consumerOffsetManager.commitOffset(remoteAddress, consumeGroup, topic, qId, nextOffset); + } + if (!consumerOrderInfoManager.checkBlock(null, topic, consumeGroup, qId, invisibleTime)) { + this.brokerController.getPopMessageProcessor().notifyMessageArriving(topic, qId, consumeGroup); + } + return; + } + + if (nextOffset == -1) { + String errorInfo = String.format("offset is illegal, key:%s %s %s, old:%d, commit:%d, next:%d, %s", + consumeGroup, topic, qId, oldOffset, ackOffset, nextOffset, channel.remoteAddress()); + POP_LOGGER.warn(errorInfo); + response.setCode(ResponseCode.MESSAGE_ILLEGAL); + response.setRemark(errorInfo); + } + } finally { + consumerLockService.unlock(consumeGroup, topic); + } + } +} 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 new file mode 100644 index 0000000..812ca90 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessor.java @@ -0,0 +1,3463 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alibaba.fastjson.JSONObject; +import com.google.common.collect.Sets; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.opentelemetry.api.common.Attributes; +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; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.SynchronousQueue; +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.auth.authentication.enums.UserType; +import org.apache.rocketmq.auth.authentication.exception.AuthenticationException; +import org.apache.rocketmq.auth.authentication.model.Subject; +import org.apache.rocketmq.auth.authentication.model.User; +import org.apache.rocketmq.auth.authorization.enums.PolicyType; +import org.apache.rocketmq.auth.authorization.exception.AuthorizationException; +import org.apache.rocketmq.auth.authorization.model.Acl; +import org.apache.rocketmq.auth.authorization.model.Resource; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.auth.converter.AclConverter; +import org.apache.rocketmq.broker.auth.converter.UserConverter; +import org.apache.rocketmq.broker.client.ClientChannelInfo; +import org.apache.rocketmq.broker.client.ConsumerGroupInfo; +import org.apache.rocketmq.broker.config.v1.RocksDBConsumerOffsetManager; +import org.apache.rocketmq.broker.config.v1.RocksDBSubscriptionGroupManager; +import org.apache.rocketmq.broker.config.v1.RocksDBTopicConfigManager; +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.metrics.BrokerMetricsManager; +import org.apache.rocketmq.broker.metrics.InvocationStatus; +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.BoundaryType; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.CheckRocksdbCqWriteResult; +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.Pair; +import org.apache.rocketmq.common.TopicAttributes; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.UnlockCallback; +import org.apache.rocketmq.common.UtilAll; +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.stats.StatsItem; +import org.apache.rocketmq.common.stats.StatsSnapshot; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.common.utils.ExceptionUtils; +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.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.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.AclInfo; +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.CreateTopicListRequestBody; +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.SubscriptionGroupList; +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.body.UserInfo; +import org.apache.rocketmq.remoting.protocol.header.CheckRocksdbCqWriteProgressRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.CloneGroupOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ConsumeMessageDirectlyResultRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.CreateAclRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.CreateTopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.CreateUserRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.DeleteAclRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.DeleteSubscriptionGroupRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.DeleteTopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.DeleteUserRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ExchangeHAInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ExchangeHAInfoResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.ExportRocksDBConfigToJsonRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetAclRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetAllProducerInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetAllTopicConfigResponseHeader; +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.GetUserRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ListAclsRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ListUsersRequestHeader; +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.UpdateAclRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateGroupForbiddenRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateUserRequestHeader; +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.RocksDBMessageStore; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.StoreType; +import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.exception.ConsumeQueueException; +import org.apache.rocketmq.store.plugin.AbstractPluginMessageStore; +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.broker.metrics.BrokerMetricsConstant.LABEL_INVOCATION_STATUS; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_IS_SYSTEM; +import static org.apache.rocketmq.remoting.protocol.RemotingCommand.buildErrorResponse; + +public class AdminBrokerProcessor implements NettyRequestProcessor { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + protected final BrokerController brokerController; + protected Set configBlackList = new HashSet<>(); + private final ExecutorService asyncExecuteWorker = new ThreadPoolExecutor(0, 4, 60L, TimeUnit.SECONDS, new SynchronousQueue<>()); + + 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 + public RemotingCommand processRequest(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + switch (request.getCode()) { + case RequestCode.UPDATE_AND_CREATE_TOPIC: + return this.updateAndCreateTopic(ctx, request); + case RequestCode.UPDATE_AND_CREATE_TOPIC_LIST: + return this.updateAndCreateTopicList(ctx, request); + case RequestCode.DELETE_TOPIC_IN_BROKER: + 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: + return this.getMaxOffset(ctx, request); + case RequestCode.GET_MIN_OFFSET: + return this.getMinOffset(ctx, request); + case RequestCode.GET_EARLIEST_MSG_STORETIME: + return this.getEarliestMsgStoretime(ctx, request); + case RequestCode.GET_BROKER_RUNTIME_INFO: + return this.getBrokerRuntimeInfo(ctx, request); + case RequestCode.LOCK_BATCH_MQ: + return this.lockBatchMQ(ctx, request); + case RequestCode.UNLOCK_BATCH_MQ: + return this.unlockBatchMQ(ctx, request); + case RequestCode.UPDATE_AND_CREATE_SUBSCRIPTIONGROUP: + return this.updateAndCreateSubscriptionGroup(ctx, request); + case RequestCode.UPDATE_AND_CREATE_SUBSCRIPTIONGROUP_LIST: + return this.updateAndCreateSubscriptionGroupList(ctx, request); + case RequestCode.GET_ALL_SUBSCRIPTIONGROUP_CONFIG: + return this.getAllSubscriptionGroup(ctx, request); + case RequestCode.DELETE_SUBSCRIPTIONGROUP: + return this.deleteSubscriptionGroup(ctx, request); + case RequestCode.GET_TOPIC_STATS_INFO: + return this.getTopicStatsInfo(ctx, request); + case RequestCode.GET_CONSUMER_CONNECTION_LIST: + 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.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: + return this.getConsumerRunningInfo(ctx, request); + case RequestCode.QUERY_CORRECTION_OFFSET: + return this.queryCorrectionOffset(ctx, request); + case RequestCode.CONSUME_MESSAGE_DIRECTLY: + return this.consumeMessageDirectly(ctx, request); + case RequestCode.CLONE_GROUP_OFFSET: + return this.cloneGroupOffset(ctx, request); + case RequestCode.VIEW_BROKER_STATS_DATA: + return ViewBrokerStatsData(ctx, request); + case RequestCode.GET_BROKER_CONSUME_STATS: + return fetchAllConsumeStatsInBroker(ctx, request); + case RequestCode.QUERY_CONSUME_QUEUE: + return queryConsumeQueue(ctx, request); + case RequestCode.CHECK_ROCKSDB_CQ_WRITE_PROGRESS: + return this.checkRocksdbCqWriteProgress(ctx, request); + case RequestCode.EXPORT_ROCKSDB_CONFIG_TO_JSON: + return this.exportRocksDBConfigToJson(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.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); + case RequestCode.AUTH_CREATE_USER: + return this.createUser(ctx, request); + case RequestCode.AUTH_UPDATE_USER: + return this.updateUser(ctx, request); + case RequestCode.AUTH_DELETE_USER: + return this.deleteUser(ctx, request); + case RequestCode.AUTH_GET_USER: + return this.getUser(ctx, request); + case RequestCode.AUTH_LIST_USER: + return this.listUser(ctx, request); + case RequestCode.AUTH_CREATE_ACL: + return this.createAcl(ctx, request); + case RequestCode.AUTH_UPDATE_ACL: + return this.updateAcl(ctx, request); + case RequestCode.AUTH_DELETE_ACL: + return this.deleteAcl(ctx, request); + case RequestCode.AUTH_GET_ACL: + return this.getAcl(ctx, request); + case RequestCode.AUTH_LIST_ACL: + return this.listAcl(ctx, request); + case RequestCode.POP_ROLLBACK: + return this.transferPopToFsStore(ctx, request); + default: + return getUnknownCmdResponse(ctx, request); + } + } + + /** + * @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().findSubscriptionGroupConfig(requestHeader.getGroup()); + if (groupConfig == null) { + LOGGER.error("No group in this broker, client: {} group: {}", ctx.channel().remoteAddress(), requestHeader.getGroup()); + response.setCode(ResponseCode.SUBSCRIPTION_GROUP_NOT_EXIST); + 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; + } + + private RemotingCommand checkRocksdbCqWriteProgress(ChannelHandlerContext ctx, RemotingCommand request) { + CheckRocksdbCqWriteResult result = new CheckRocksdbCqWriteResult(); + result.setCheckStatus(CheckRocksdbCqWriteResult.CheckStatus.CHECK_IN_PROGRESS.getValue()); + Runnable runnable = () -> { + try { + CheckRocksdbCqWriteResult checkResult = doCheckRocksdbCqWriteProgress(ctx, request); + LOGGER.info("checkRocksdbCqWriteProgress result: {}", JSON.toJSONString(checkResult)); + } catch (Exception e) { + LOGGER.error("checkRocksdbCqWriteProgress error", e); + } + }; + asyncExecuteWorker.submit(runnable); + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + response.setBody(JSON.toJSONBytes(result)); + return response; + } + + private RemotingCommand exportRocksDBConfigToJson(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + ExportRocksDBConfigToJsonRequestHeader requestHeader = request.decodeCommandCustomHeader(ExportRocksDBConfigToJsonRequestHeader.class); + List configTypes = requestHeader.fetchConfigType(); + List> futureList = new ArrayList<>(configTypes.size()); + for (ExportRocksDBConfigToJsonRequestHeader.ConfigType type : configTypes) { + switch (type) { + case TOPICS: + if (this.brokerController.getTopicConfigManager() instanceof RocksDBTopicConfigManager) { + RocksDBTopicConfigManager rocksDBTopicConfigManager = (RocksDBTopicConfigManager) this.brokerController.getTopicConfigManager(); + futureList.add(CompletableFuture.runAsync(rocksDBTopicConfigManager::exportToJson, asyncExecuteWorker)); + } + break; + case SUBSCRIPTION_GROUPS: + if (this.brokerController.getSubscriptionGroupManager() instanceof RocksDBSubscriptionGroupManager) { + RocksDBSubscriptionGroupManager rocksDBSubscriptionGroupManager = (RocksDBSubscriptionGroupManager) this.brokerController.getSubscriptionGroupManager(); + futureList.add(CompletableFuture.runAsync(rocksDBSubscriptionGroupManager::exportToJson, asyncExecuteWorker)); + } + break; + case CONSUMER_OFFSETS: + if (this.brokerController.getConsumerOffsetManager() instanceof RocksDBConsumerOffsetManager) { + RocksDBConsumerOffsetManager rocksDBConsumerOffsetManager = (RocksDBConsumerOffsetManager) this.brokerController.getConsumerOffsetManager(); + futureList.add(CompletableFuture.runAsync(rocksDBConsumerOffsetManager::exportToJson, asyncExecuteWorker)); + } + break; + default: + break; + } + } + + try { + CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).join(); + } catch (CompletionException e) { + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(String.valueOf(e)); + return response; + } + + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + response.setRemark("export done."); + return response; + } + + @Override + public boolean rejectRequest() { + return false; + } + + private synchronized RemotingCommand updateAndCreateTopic(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + long startTime = System.currentTimeMillis(); + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + final CreateTopicRequestHeader requestHeader = + (CreateTopicRequestHeader) request.decodeCommandCustomHeader(CreateTopicRequestHeader.class); + + LOGGER.info("Broker receive request to update or create topic={}, caller address={}", + requestHeader.getTopic(), RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + + String topic = requestHeader.getTopic(); + + long executionTime; + try { + TopicValidator.ValidateTopicResult result = TopicValidator.validateTopic(topic); + if (!result.isValid()) { + response.setCode(ResponseCode.INVALID_PARAMETER); + response.setRemark(result.getRemark()); + return response; + } + if (brokerController.getBrokerConfig().isValidateSystemTopicWhenUpdateTopic()) { + if (TopicValidator.isSystemTopic(topic)) { + response.setCode(ResponseCode.INVALID_PARAMETER); + 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 (!brokerController.getBrokerConfig().isEnableMixedMessageType() && topicConfig.getAttributes() != null) { + // Get attribute by key with prefix sign + String msgTypeAttrKey = AttributeParser.ATTR_ADD_PLUS_SIGN + TopicAttributes.TOPIC_MESSAGE_TYPE_ATTRIBUTE.getName(); + String msgTypeAttrValue = topicConfig.getAttributes().get(msgTypeAttrKey); + if (msgTypeAttrValue != null && msgTypeAttrValue.equals(TopicMessageType.MIXED.getValue())) { + response.setCode(ResponseCode.INVALID_PARAMETER); + 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; + } + + 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); + } catch (Exception e) { + LOGGER.error("Update / create topic failed for [{}]", request, e); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(e.getMessage()); + return response; + } finally { + executionTime = System.currentTimeMillis() - startTime; + InvocationStatus status = response.getCode() == ResponseCode.SUCCESS ? + InvocationStatus.SUCCESS : InvocationStatus.FAILURE; + Attributes attributes = BrokerMetricsManager.newAttributesBuilder() + .put(LABEL_INVOCATION_STATUS, status.getName()) + .put(LABEL_IS_SYSTEM, TopicValidator.isSystemTopic(topic)) + .build(); + BrokerMetricsManager.topicCreateExecuteTime.record(executionTime, attributes); + } + LOGGER.info("executionTime of create topic:{} is {} ms", topic, executionTime); + return response; + } + + private synchronized RemotingCommand updateAndCreateTopicList(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + long startTime = System.currentTimeMillis(); + + final CreateTopicListRequestBody requestBody = CreateTopicListRequestBody.decode(request.getBody(), CreateTopicListRequestBody.class); + List topicConfigList = requestBody.getTopicConfigList(); + + StringBuilder builder = new StringBuilder(); + for (TopicConfig topicConfig : topicConfigList) { + builder.append(topicConfig.getTopicName()).append(";"); + } + String topicNames = builder.toString(); + LOGGER.info("AdminBrokerProcessor#updateAndCreateTopicList: topicNames: {}, called by {}", topicNames, RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + long executionTime; + + try { + // Valid topics + for (TopicConfig topicConfig : topicConfigList) { + String topic = topicConfig.getTopicName(); + TopicValidator.ValidateTopicResult result = TopicValidator.validateTopic(topic); + if (!result.isValid()) { + response.setCode(ResponseCode.INVALID_PARAMETER); + response.setRemark(result.getRemark()); + return response; + } + if (brokerController.getBrokerConfig().isValidateSystemTopicWhenUpdateTopic()) { + if (TopicValidator.isSystemTopic(topic)) { + response.setCode(ResponseCode.INVALID_PARAMETER); + response.setRemark("The topic[" + topic + "] is conflict with system topic."); + return response; + } + } + if (!brokerController.getBrokerConfig().isEnableMixedMessageType() && topicConfig.getAttributes() != null) { + // Get attribute by key with prefix sign + String msgTypeAttrKey = AttributeParser.ATTR_ADD_PLUS_SIGN + TopicAttributes.TOPIC_MESSAGE_TYPE_ATTRIBUTE.getName(); + String msgTypeAttrValue = topicConfig.getAttributes().get(msgTypeAttrKey); + if (msgTypeAttrValue != null && msgTypeAttrValue.equals(TopicMessageType.MIXED.getValue())) { + response.setCode(ResponseCode.INVALID_PARAMETER); + 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={}", + topic, RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + response.setCode(ResponseCode.SUCCESS); + return response; + } + } + + this.brokerController.getTopicConfigManager().updateTopicConfigList(topicConfigList); + if (brokerController.getBrokerConfig().isEnableSingleTopicRegister()) { + for (TopicConfig topicConfig : topicConfigList) { + this.brokerController.registerSingleTopicAll(topicConfig); + } + } else { + this.brokerController.registerIncrementBrokerData(topicConfigList, this.brokerController.getTopicConfigManager().getDataVersion()); + } + response.setCode(ResponseCode.SUCCESS); + } catch (Exception e) { + LOGGER.error("Update / create topic failed for [{}]", request, e); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(e.getMessage()); + return response; + } finally { + executionTime = System.currentTimeMillis() - startTime; + InvocationStatus status = response.getCode() == ResponseCode.SUCCESS ? + InvocationStatus.SUCCESS : InvocationStatus.FAILURE; + Attributes attributes = BrokerMetricsManager.newAttributesBuilder() + .put(LABEL_INVOCATION_STATUS, status.getName()) + .put(LABEL_IS_SYSTEM, TopicValidator.isSystemTopic(topicNames)) + .build(); + BrokerMetricsManager.topicCreateExecuteTime.record(executionTime, attributes); + } + LOGGER.info("executionTime of all topics:{} is {} ms", topicNames, executionTime); + 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.INVALID_PARAMETER); + response.setRemark(result.getRemark()); + return response; + } + if (brokerController.getBrokerConfig().isValidateSystemTopicWhenUpdateTopic()) { + if (TopicValidator.isSystemTopic(topic)) { + response.setCode(ResponseCode.INVALID_PARAMETER); + response.setRemark("The topic[" + topic + "] is conflict with system topic."); + return response; + } + } + boolean force = requestHeader.getForce() != null && requestHeader.getForce(); + + 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()); + + try { + this.brokerController.getTopicConfigManager().updateTopicConfig(topicConfig); + + 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 synchronized RemotingCommand deleteTopic(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + DeleteTopicRequestHeader requestHeader = + (DeleteTopicRequestHeader) request.decodeCommandCustomHeader(DeleteTopicRequestHeader.class); + + LOGGER.info("AdminBrokerProcessor#deleteTopic: broker receive request to delete topic={}, caller={}", + requestHeader.getTopic(), RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + + String topic = requestHeader.getTopic(); + + if (UtilAll.isBlank(topic)) { + response.setCode(ResponseCode.INVALID_PARAMETER); + response.setRemark("The specified topic is blank."); + return response; + } + + if (brokerController.getBrokerConfig().isValidateSystemTopicWhenUpdateTopic()) { + if (TopicValidator.isSystemTopic(topic)) { + response.setCode(ResponseCode.INVALID_PARAMETER); + response.setRemark("The topic[" + topic + "] is conflict with system topic."); + return response; + } + } + + List topicsToClean = new ArrayList<>(); + topicsToClean.add(topic); + + if (brokerController.getBrokerConfig().isClearRetryTopicWhenDeleteTopic()) { + final Set groups = this.brokerController.getConsumerOffsetManager().whichGroupByTopic(topic); + for (String group : groups) { + final String popRetryTopicV2 = KeyBuilder.buildPopRetryTopic(topic, group, true); + if (brokerController.getTopicConfigManager().selectTopicConfig(popRetryTopicV2) != null) { + topicsToClean.add(popRetryTopicV2); + } + final String popRetryTopicV1 = KeyBuilder.buildPopRetryTopicV1(topic, group); + if (brokerController.getTopicConfigManager().selectTopicConfig(popRetryTopicV1) != null) { + topicsToClean.add(popRetryTopicV1); + } + } + } + + try { + for (String topicToClean : topicsToClean) { + // delete topic + deleteTopicInBroker(topicToClean); + } + } 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)); + this.brokerController.getMessageStore().getTimerMessageStore().getTimerMetrics().removeTimingCount(topic); + } + + 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(); + + 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) { + LOGGER.error("", e); + + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("UnsupportedEncodingException " + e.getMessage()); + return response; + } + } else { + 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; + } + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + + return response; + } + + 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())); + + 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.forEach((key, value) -> { + try { + String consumerGroup = String.valueOf(key); + Long threshold = Long.valueOf(String.valueOf(value)); + this.brokerController.getColdDataCgCtrService() + .addOrUpdateGroupConfig(consumerGroup, threshold); + } catch (Exception e) { + LOGGER.error("updateColdDataFlowCtrGroupConfig properties on entry error, key: {}, val: {}", + key, value, 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.INVALID_PARAMETER); + 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) { + try { + String bodyStr = new String(body, MixAll.DEFAULT_CHARSET); + Properties properties = MixAll.string2Properties(bodyStr); + if (properties != null) { + 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")) { + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + this.brokerController.getTopicConfigManager().getDataVersion().nextVersion(stateMachineVersion); + this.brokerController.registerBrokerAll(false, false, true); + } + + } else { + LOGGER.error("string2Properties error"); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("string2Properties error"); + return response; + } + } catch (UnsupportedEncodingException e) { + LOGGER.error("AdminBrokerProcessor#updateBrokerConfig: unexpected error, caller={}", + callerAddress, e); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("UnsupportedEncodingException " + e); + return response; + } + } + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private RemotingCommand getBrokerConfig(ChannelHandlerContext ctx, RemotingCommand request) { + + final RemotingCommand response = RemotingCommand.createResponseCommand(GetBrokerConfigResponseHeader.class); + final GetBrokerConfigResponseHeader responseHeader = (GetBrokerConfigResponseHeader) response.readCustomHeader(); + + String content = this.brokerController.getConfiguration().getAllConfigsFormatString(); + if (content != null && content.length() > 0) { + try { + response.setBody(content.getBytes(MixAll.DEFAULT_CHARSET)); + } catch (UnsupportedEncodingException e) { + LOGGER.error("AdminBrokerProcessor#getBrokerConfig: unexpected error, caller={}", + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), e); + + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("UnsupportedEncodingException " + e); + return response; + } + } + + responseHeader.setVersion(this.brokerController.getConfiguration().getDataVersionJson()); + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + 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); + final SearchOffsetResponseHeader responseHeader = (SearchOffsetResponseHeader) response.readCustomHeader(); + 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.getBoundaryType()); + + responseHeader.setOffset(offset); + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + 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); + final GetMaxOffsetResponseHeader responseHeader = (GetMaxOffsetResponseHeader) response.readCustomHeader(); + final GetMaxOffsetRequestHeader requestHeader = request.decodeCommandCustomHeader(GetMaxOffsetRequestHeader.class); + + TopicQueueMappingContext mappingContext = this.brokerController.getTopicQueueMappingManager().buildTopicQueueMappingContext(requestHeader); + RemotingCommand rewriteResult = rewriteRequestForStaticTopic(requestHeader, mappingContext); + if (rewriteResult != null) { + return rewriteResult; + } + + try { + long offset = this.brokerController.getMessageStore().getMaxOffsetInQueue(requestHeader.getTopic(), requestHeader.getQueueId()); + responseHeader.setOffset(offset); + } catch (ConsumeQueueException e) { + throw new RemotingCommandException("Failed to get max offset in queue", e); + } + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + 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 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()); + } + } + + private RemotingCommand rewriteRequestForStaticTopic(GetEarliestMsgStoretimeRequestHeader requestHeader, + TopicQueueMappingContext mappingContext) { + 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", 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, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(GetEarliestMsgStoretimeResponseHeader.class); + final GetEarliestMsgStoretimeResponseHeader responseHeader = (GetEarliestMsgStoretimeResponseHeader) response.readCustomHeader(); + 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()); + + responseHeader.setTimestamp(timestamp); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private RemotingCommand getBrokerRuntimeInfo(ChannelHandlerContext ctx, RemotingCommand request) + throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + HashMap runtimeInfo = this.prepareRuntimeInfo(); + KVTable kvTable = new KVTable(); + kvTable.setTable(runtimeInfo); + + byte[] body = kvTable.encode(); + response.setBody(body); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private RemotingCommand lockBatchMQ(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + LockBatchRequestBody requestBody = LockBatchRequestBody.decode(request.getBody(), LockBatchRequestBody.class); + + 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); + + response.setBody(responseBody.encode()); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private RemotingCommand unlockBatchMQ(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + UnlockBatchRequestBody requestBody = UnlockBatchRequestBody.decode(request.getBody(), UnlockBatchRequestBody.class); + + 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); + return response; + } + + private RemotingCommand updateAndCreateSubscriptionGroup(ChannelHandlerContext ctx, RemotingCommand request) + throws RemotingCommandException { + long startTime = System.currentTimeMillis(); + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + LOGGER.info("AdminBrokerProcessor#updateAndCreateSubscriptionGroup called by {}", + RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + + SubscriptionGroupConfig config = RemotingSerializable.decode(request.getBody(), SubscriptionGroupConfig.class); + if (config != null) { + this.brokerController.getSubscriptionGroupManager().updateSubscriptionGroupConfig(config); + } + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + long executionTime = System.currentTimeMillis() - startTime; + LOGGER.info("executionTime of create subscriptionGroup:{} is {} ms", config.getGroupName(), executionTime); + InvocationStatus status = response.getCode() == ResponseCode.SUCCESS ? + InvocationStatus.SUCCESS : InvocationStatus.FAILURE; + Attributes attributes = BrokerMetricsManager.newAttributesBuilder() + .put(LABEL_INVOCATION_STATUS, status.getName()) + .build(); + BrokerMetricsManager.consumerGroupCreateExecuteTime.record(executionTime, attributes); + return response; + } + + private RemotingCommand updateAndCreateSubscriptionGroupList(ChannelHandlerContext ctx, RemotingCommand request) { + final long startTime = System.nanoTime(); + + final SubscriptionGroupList subscriptionGroupList = SubscriptionGroupList.decode(request.getBody(), SubscriptionGroupList.class); + final List groupConfigList = subscriptionGroupList.getGroupConfigList(); + + final StringBuilder builder = new StringBuilder(); + for (SubscriptionGroupConfig config : groupConfigList) { + builder.append(config.getGroupName()).append(";"); + } + final String groupNames = builder.toString(); + LOGGER.info("AdminBrokerProcessor#updateAndCreateSubscriptionGroupList: groupNames: {}, called by {}", + groupNames, + RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + try { + this.brokerController.getSubscriptionGroupManager().updateSubscriptionGroupConfigList(groupConfigList); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + } finally { + long executionTime = (System.nanoTime() - startTime) / 1000000L; + LOGGER.info("executionTime of create updateAndCreateSubscriptionGroupList: {} is {} ms", groupNames, executionTime); + InvocationStatus status = response.getCode() == ResponseCode.SUCCESS ? + InvocationStatus.SUCCESS : InvocationStatus.FAILURE; + Attributes attributes = BrokerMetricsManager.newAttributesBuilder() + .put(LABEL_INVOCATION_STATUS, status.getName()) + .build(); + BrokerMetricsManager.consumerGroupCreateExecuteTime.record(executionTime, attributes); + } + + return response; + } + + private void initConsumerOffset(String clientHost, String groupName, int mode, TopicConfig topicConfig) + throws ConsumeQueueException { + 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); + String content = this.brokerController.getSubscriptionGroupManager().encode(); + if (content != null && content.length() > 0) { + try { + response.setBody(content.getBytes(MixAll.DEFAULT_CHARSET)); + } catch (UnsupportedEncodingException e) { + LOGGER.error("UnsupportedEncodingException getAllSubscriptionGroup", e); + + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("UnsupportedEncodingException " + e.getMessage()); + return response; + } + } else { + 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; + } + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + + return response; + } + + private RemotingCommand deleteSubscriptionGroup(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + DeleteSubscriptionGroupRequestHeader requestHeader = + (DeleteSubscriptionGroupRequestHeader) request.decodeCommandCustomHeader(DeleteSubscriptionGroupRequestHeader.class); + + 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; + } + + private RemotingCommand getTopicStatsInfo(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + final GetTopicStatsInfoRequestHeader requestHeader = request.decodeCommandCustomHeader(GetTopicStatsInfoRequestHeader.class); + + final String topic = requestHeader.getTopic(); + TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(topic); + if (null == topicConfig) { + response.setCode(ResponseCode.TOPIC_NOT_EXIST); + response.setRemark("topic[" + topic + "] not exist"); + return response; + } + + TopicStatsTable topicStatsTable = new TopicStatsTable(); + + int maxQueueNums = Math.max(topicConfig.getWriteQueueNums(), topicConfig.getReadQueueNums()); + try { + for (int i = 0; i < maxQueueNums; i++) { + MessageQueue mq = new MessageQueue(); + mq.setTopic(topic); + mq.setBrokerName(this.brokerController.getBrokerConfig().getBrokerName()); + mq.setQueueId(i); + + TopicOffset topicOffset = new TopicOffset(); + long min = this.brokerController.getMessageStore().getMinOffsetInQueue(topic, i); + if (min < 0) { + min = 0; + } + + long max = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, i); + if (max < 0) { + max = 0; + } + + long timestamp = 0; + if (max > 0) { + timestamp = this.brokerController.getMessageStore().getMessageStoreTimeStamp(topic, i, max - 1); + } + + topicOffset.setMinOffset(min); + topicOffset.setMaxOffset(max); + topicOffset.setLastUpdateTimestamp(timestamp); + + topicStatsTable.getOffsetTable().put(mq, topicOffset); + } + + topicStatsTable.setTopicPutTps(this.brokerController.getBrokerStatsManager().tpsTopicPutNums(requestHeader.getTopic())); + byte[] body = topicStatsTable.encode(); + response.setBody(body); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + } catch (ConsumeQueueException e) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(e.getMessage()); + } + + return response; + } + + private RemotingCommand getConsumerConnectionList(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + final GetConsumerConnectionListRequestHeader requestHeader = + (GetConsumerConnectionListRequestHeader) request.decodeCommandCustomHeader(GetConsumerConnectionListRequestHeader.class); + + ConsumerGroupInfo consumerGroupInfo = + this.brokerController.getConsumerManager().getConsumerGroupInfo(requestHeader.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[" + requestHeader.getConsumerGroup() + "] not online"); + 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); + final GetProducerConnectionListRequestHeader requestHeader = + (GetProducerConnectionListRequestHeader) request.decodeCommandCustomHeader(GetProducerConnectionListRequestHeader.class); + + ProducerConnection bodydata = new ProducerConnection(); + Map channelInfoHashMap = + this.brokerController.getProducerManager().getGroupChannelTable().get(requestHeader.getProducerGroup()); + if (channelInfoHashMap != null) { + Iterator> it = channelInfoHashMap.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.SYSTEM_ERROR); + response.setRemark("the producer group[" + requestHeader.getProducerGroup() + "] not exist"); + return response; + } + + private RemotingCommand getConsumeStats(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + try { + final GetConsumeStatsRequestHeader requestHeader = request.decodeCommandCustomHeader(GetConsumeStatsRequestHeader.class); + List topicListProvided = requestHeader.fetchTopicList(); + String topicProvided = requestHeader.getTopic(); + String group = requestHeader.getConsumerGroup(); + + ConsumeStats consumeStats = new ConsumeStats(); + Set topicsForCollecting = getTopicsForCollectingConsumeStats(topicListProvided, topicProvided, group); + + for (String topic : topicsForCollecting) { + TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(topic); + if (null == topicConfig) { + LOGGER.warn("AdminBrokerProcessor#getConsumeStats: topic config does not exist, topic={}", topic); + continue; + } + + TopicQueueMappingDetail mappingDetail = this.brokerController.getTopicQueueMappingManager().getTopicQueueMapping(topic); + for (int i = 0; i < topicConfig.getReadQueueNums(); i++) { + MessageQueue mq = new MessageQueue(); + mq.setTopic(topic); + mq.setBrokerName(this.brokerController.getBrokerConfig().getBrokerName()); + mq.setQueueId(i); + + OffsetWrapper offsetWrapper = new OffsetWrapper(); + + long brokerOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, i); + 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 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) { + long lastTimestamp = this.brokerController.getMessageStore().getMessageStoreTimeStamp(topic, i, timeOffset); + if (lastTimestamp > 0) { + offsetWrapper.setLastTimestamp(lastTimestamp); + } + } + + consumeStats.getOffsetTable().put(mq, offsetWrapper); + } + + double consumeTps = this.brokerController.getBrokerStatsManager().tpsGroupGetNums(requestHeader.getConsumerGroup(), topic); + + consumeTps += consumeStats.getConsumeTps(); + consumeStats.setConsumeTps(consumeTps); + } + + byte[] body = consumeStats.encode(); + response.setBody(body); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + } catch (ConsumeQueueException e) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(e.getMessage()); + } + return response; + } + + private Set getTopicsForCollectingConsumeStats(List topicListProvided, String topicProvided, + String group) { + Set topicsForCollecting = new HashSet<>(); + if (!topicListProvided.isEmpty()) { + // if topic list is provided, only collect the topics in the list + // and ignore subscription check + topicsForCollecting.addAll(topicListProvided); + } else { + // In order to be compatible with the old logic, + // even if the topic has been provided here, the subscription will be checked. + if (UtilAll.isBlank(topicProvided)) { + topicsForCollecting.addAll( + this.brokerController.getConsumerOffsetManager().whichTopicByConsumer(group)); + } else { + topicsForCollecting.add(topicProvided); + } + int subscriptionCount = this.brokerController.getConsumerManager().findSubscriptionDataCount(group); + Iterator iterator = topicsForCollecting.iterator(); + while (iterator.hasNext()) { + String topic = iterator.next(); + SubscriptionData findSubscriptionData = this.brokerController.getConsumerManager().findSubscriptionData(group, topic); + if (findSubscriptionData == null && subscriptionCount > 0) { + LOGGER.warn( + "AdminBrokerProcessor#getConsumeStats: topic does not exist in consumer group's subscription, topic={}, consumer group={}", + topic, group); + iterator.remove(); + } + } + } + return topicsForCollecting; + } + + private RemotingCommand getAllConsumerOffset(ChannelHandlerContext ctx, RemotingCommand request) { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + String content = this.brokerController.getConsumerOffsetManager().encode(); + if (content != null && content.length() > 0) { + try { + response.setBody(content.getBytes(MixAll.DEFAULT_CHARSET)); + } catch (UnsupportedEncodingException e) { + LOGGER.error("get all consumer offset from master error.", e); + + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("UnsupportedEncodingException " + e.getMessage()); + return response; + } + } else { + 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; + } + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + + return response; + } + + private RemotingCommand getAllDelayOffset(ChannelHandlerContext ctx, RemotingCommand request) { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + String content = this.brokerController.getScheduleMessageService().encode(); + if (content != null && content.length() > 0) { + try { + response.setBody(content.getBytes(MixAll.DEFAULT_CHARSET)); + } catch (UnsupportedEncodingException 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 { + 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; + } + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + + 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.isEmpty()) { + 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); + 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) { + case CPP: + isC = true; + break; + } + return this.brokerController.getBroker2Client().resetOffset(requestHeader.getTopic(), requestHeader.getGroup(), + requestHeader.getTimestamp(), requestHeader.isForce(), isC); + } + + private Long searchOffsetByTimestamp(String topic, int queueId, long timestamp) throws ConsumeQueueException { + 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().selectTopicConfig(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; + } + + try { + 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); + } + } + } catch (ConsumeQueueException e) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(e.getMessage()); + return response; + } + + 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()) { + if (brokerController.getPopInflightMessageCounter() != null) { + brokerController.getPopInflightMessageCounter().clearInFlightMessageNum(topic, group, entry.getKey()); + } + if (brokerController.getBrokerConfig().isPopConsumerKVServiceEnable()) { + brokerController.getPopConsumerService().clearCache(group, topic, entry.getKey()); + brokerController.getConsumerOffsetManager().clearPullOffset(group, topic); + } + 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); + + 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(), + requestHeader.getClientAddr()); + } + + private RemotingCommand queryTopicConsumeByWho(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + QueryTopicConsumeByWhoRequestHeader requestHeader = + (QueryTopicConsumeByWhoRequestHeader) request.decodeCommandCustomHeader(QueryTopicConsumeByWhoRequestHeader.class); + + HashSet groups = this.brokerController.getConsumerManager().queryTopicConsumeByWho(requestHeader.getTopic()); + + Set groupInOffset = this.brokerController.getConsumerOffsetManager().whichGroupByTopic(requestHeader.getTopic()); + if (groupInOffset != null && !groupInOffset.isEmpty()) { + groups.addAll(groupInOffset); + } + + GroupList groupList = new GroupList(); + groupList.setGroupList(groups); + byte[] body = groupList.encode(); + + response.setBody(body); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + 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(null); + QuerySubscriptionByConsumerRequestHeader requestHeader = + (QuerySubscriptionByConsumerRequestHeader) request.decodeCommandCustomHeader(QuerySubscriptionByConsumerRequestHeader.class); + + SubscriptionData subscriptionData = this.brokerController.getConsumerManager() + .findSubscriptionData(requestHeader.getGroup(), requestHeader.getTopic()); + + 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, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + QueryConsumeTimeSpanRequestHeader requestHeader = request.decodeCommandCustomHeader(QueryConsumeTimeSpanRequestHeader.class); + + final String topic = requestHeader.getTopic(); + TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(topic); + if (null == topicConfig) { + response.setCode(ResponseCode.TOPIC_NOT_EXIST); + response.setRemark("topic[" + topic + "] not exist"); + return response; + } + + List timeSpanSet = new ArrayList<>(); + for (int i = 0; i < topicConfig.getWriteQueueNums(); i++) { + QueueTimeSpan timeSpan = new QueueTimeSpan(); + MessageQueue mq = new MessageQueue(); + mq.setTopic(topic); + mq.setBrokerName(this.brokerController.getBrokerConfig().getBrokerName()); + mq.setQueueId(i); + timeSpan.setMessageQueue(mq); + + long minTime = this.brokerController.getMessageStore().getEarliestMessageTime(topic, i); + timeSpan.setMinTimeStamp(minTime); + + long max; + try { + max = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, i); + } catch (ConsumeQueueException e) { + throw new RemotingCommandException("Failed to get max offset in queue", e); + } + long maxTime = this.brokerController.getMessageStore().getMessageStoreTimeStamp(topic, i, max - 1); + timeSpan.setMaxTimeStamp(maxTime); + + long consumeTime; + long consumerOffset = this.brokerController.getConsumerOffsetManager().queryOffset( + requestHeader.getGroup(), topic, i); + if (consumerOffset > 0) { + consumeTime = this.brokerController.getMessageStore().getMessageStoreTimeStamp(topic, i, consumerOffset - 1); + } else { + consumeTime = minTime; + } + timeSpan.setConsumeTimeStamp(consumeTime); + + long maxBrokerOffset; + try { + maxBrokerOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(requestHeader.getTopic(), i); + } catch (ConsumeQueueException e) { + throw new RemotingCommandException("Failed to get max offset in queue", e); + } + if (consumerOffset < maxBrokerOffset) { + long nextTime = this.brokerController.getMessageStore().getMessageStoreTimeStamp(topic, i, consumerOffset); + timeSpan.setDelayTime(System.currentTimeMillis() - nextTime); + } + timeSpanSet.add(timeSpan); + } + + QueryConsumeTimeSpanBody queryConsumeTimeSpanBody = new QueryConsumeTimeSpanBody(); + queryConsumeTimeSpanBody.setConsumeTimeSpanSet(timeSpanSet); + response.setBody(queryConsumeTimeSpanBody.encode()); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private RemotingCommand getSystemTopicListFromBroker(ChannelHandlerContext ctx, RemotingCommand request) + throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + Set topics = TopicValidator.getSystemTopicSet(); + TopicList topicList = new TopicList(); + topicList.setTopicList(topics); + response.setBody(topicList.encode()); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + public RemotingCommand cleanExpiredConsumeQueue() { + 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().executeDeleteFilesManually(); + LOGGER.warn("invoke deleteExpiredCommitLog end."); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + public RemotingCommand cleanUnusedTopic() { + LOGGER.warn("invoke cleanUnusedTopic start."); + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + brokerController.getMessageStore().cleanUnusedTopic(brokerController.getTopicConfigManager().getTopicConfigTable().keySet()); + LOGGER.warn("invoke cleanUnusedTopic end."); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private RemotingCommand getConsumerRunningInfo(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final GetConsumerRunningInfoRequestHeader requestHeader = + (GetConsumerRunningInfoRequestHeader) request.decodeCommandCustomHeader(GetConsumerRunningInfoRequestHeader.class); + + return this.callConsumer(RequestCode.GET_CONSUMER_RUNNING_INFO, request, requestHeader.getConsumerGroup(), + requestHeader.getClientId()); + } + + private RemotingCommand queryCorrectionOffset(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + QueryCorrectionOffsetHeader requestHeader = + (QueryCorrectionOffsetHeader) request.decodeCommandCustomHeader(QueryCorrectionOffsetHeader.class); + + Map correctionOffset = this.brokerController.getConsumerOffsetManager() + .queryMinOffsetInAllGroup(requestHeader.getTopic(), requestHeader.getFilterGroups()); + + Map compareOffset = + this.brokerController.getConsumerOffsetManager().queryOffset(requestHeader.getCompareGroup(), requestHeader.getTopic()); + + if (compareOffset != null && !compareOffset.isEmpty()) { + for (Map.Entry entry : compareOffset.entrySet()) { + Integer queueId = entry.getKey(); + correctionOffset.put(queueId, + correctionOffset.get(queueId) > entry.getValue() ? Long.MAX_VALUE : correctionOffset.get(queueId)); + } + } + + QueryCorrectionOffsetBody body = new QueryCorrectionOffsetBody(); + body.setCorrectionOffsets(correctionOffset); + response.setBody(body.encode()); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private RemotingCommand consumeMessageDirectly(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + 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().findSubscriptionGroupConfig(requestHeader.getConsumerGroup()); + if (groupConfig != null) { + request.addExtField("groupSysFlag", String.valueOf(groupConfig.getGroupSysFlag())); + } + } + SelectMappedBufferResult selectMappedBufferResult = null; + try { + MessageId messageId = MessageDecoder.decodeMessageId(requestHeader.getMsgId()); + selectMappedBufferResult = this.brokerController.getMessageStore().selectOneMessageByOffset(messageId.getOffset()); + + byte[] body = new byte[selectMappedBufferResult.getSize()]; + selectMappedBufferResult.getByteBuffer().get(body); + request.setBody(body); + } catch (UnknownHostException e) { + } finally { + if (selectMappedBufferResult != null) { + selectMappedBufferResult.release(); + } + } + + return this.callConsumer(RequestCode.CONSUME_MESSAGE_DIRECTLY, request, requestHeader.getConsumerGroup(), + requestHeader.getClientId()); + } + + private RemotingCommand cloneGroupOffset(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + CloneGroupOffsetRequestHeader requestHeader = + (CloneGroupOffsetRequestHeader) request.decodeCommandCustomHeader(CloneGroupOffsetRequestHeader.class); + + Set topics; + if (UtilAll.isBlank(requestHeader.getTopic())) { + topics = this.brokerController.getConsumerOffsetManager().whichTopicByConsumer(requestHeader.getSrcGroup()); + } else { + topics = new HashSet<>(); + topics.add(requestHeader.getTopic()); + } + + for (String topic : topics) { + TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(topic); + if (null == topicConfig) { + LOGGER.warn("[cloneGroupOffset], topic config not exist, {}", topic); + continue; + } + + if (!requestHeader.isOffline()) { + + SubscriptionData findSubscriptionData = + this.brokerController.getConsumerManager().findSubscriptionData(requestHeader.getSrcGroup(), topic); + if (this.brokerController.getConsumerManager().findSubscriptionDataCount(requestHeader.getSrcGroup()) > 0 + && findSubscriptionData == null) { + LOGGER.warn( + "AdminBrokerProcessor#cloneGroupOffset: topic does not exist in consumer group's " + + "subscription, topic={}, consumer group={}", topic, requestHeader.getSrcGroup()); + continue; + } + } + + this.brokerController.getConsumerOffsetManager().cloneOffset(requestHeader.getSrcGroup(), requestHeader.getDestGroup(), + requestHeader.getTopic()); + } + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private RemotingCommand ViewBrokerStatsData(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final ViewBrokerStatsDataRequestHeader requestHeader = + (ViewBrokerStatsDataRequestHeader) request.decodeCommandCustomHeader(ViewBrokerStatsDataRequestHeader.class); + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + MessageStore messageStore = this.brokerController.getMessageStore(); + + StatsItem statsItem = messageStore.getBrokerStatsManager().getStatsItem(requestHeader.getStatsName(), requestHeader.getStatsKey()); + if (null == statsItem) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(String.format("The stats <%s> <%s> not exist", requestHeader.getStatsName(), requestHeader.getStatsKey())); + return response; + } + + BrokerStatsData brokerStatsData = new BrokerStatsData(); + + { + BrokerStatsItem it = new BrokerStatsItem(); + StatsSnapshot ss = statsItem.getStatsDataInMinute(); + it.setSum(ss.getSum()); + it.setTps(ss.getTps()); + it.setAvgpt(ss.getAvgpt()); + brokerStatsData.setStatsMinute(it); + } + + { + BrokerStatsItem it = new BrokerStatsItem(); + StatsSnapshot ss = statsItem.getStatsDataInHour(); + it.setSum(ss.getSum()); + it.setTps(ss.getTps()); + it.setAvgpt(ss.getAvgpt()); + brokerStatsData.setStatsHour(it); + } + + { + BrokerStatsItem it = new BrokerStatsItem(); + StatsSnapshot ss = statsItem.getStatsDataInDay(); + it.setSum(ss.getSum()); + it.setTps(ss.getTps()); + it.setAvgpt(ss.getAvgpt()); + brokerStatsData.setStatsDay(it); + } + + response.setBody(brokerStatsData.encode()); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private RemotingCommand fetchAllConsumeStatsInBroker(ChannelHandlerContext ctx, RemotingCommand request) + throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + GetConsumeStatsInBrokerHeader requestHeader = request.decodeCommandCustomHeader(GetConsumeStatsInBrokerHeader.class); + boolean isOrder = requestHeader.isOrder(); + ConcurrentMap subscriptionGroups = + brokerController.getSubscriptionGroupManager().getSubscriptionGroupTable(); + + List>> brokerConsumeStatsList = + new ArrayList<>(); + + long totalDiff = 0L; + long totalInflightDiff = 0L; + for (String group : subscriptionGroups.keySet()) { + Map> subscripTopicConsumeMap = new HashMap<>(); + Set topics = this.brokerController.getConsumerOffsetManager().whichTopicByConsumer(group); + List consumeStatsList = new ArrayList<>(); + for (String topic : topics) { + ConsumeStats consumeStats = new ConsumeStats(); + TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(topic); + if (null == topicConfig) { + LOGGER.warn( + "AdminBrokerProcessor#fetchAllConsumeStatsInBroker: topic config does not exist, topic={}", + topic); + continue; + } + + if (isOrder && !topicConfig.isOrder()) { + continue; + } + + { + SubscriptionData findSubscriptionData = this.brokerController.getConsumerManager().findSubscriptionData(group, topic); + + if (null == findSubscriptionData + && this.brokerController.getConsumerManager().findSubscriptionDataCount(group) > 0) { + LOGGER.warn( + "AdminBrokerProcessor#fetchAllConsumeStatsInBroker: topic does not exist in consumer " + + "group's subscription, topic={}, consumer group={}", topic, group); + continue; + } + } + + for (int i = 0; i < topicConfig.getWriteQueueNums(); i++) { + MessageQueue mq = new MessageQueue(); + mq.setTopic(topic); + mq.setBrokerName(this.brokerController.getBrokerConfig().getBrokerName()); + mq.setQueueId(i); + OffsetWrapper offsetWrapper = new OffsetWrapper(); + long brokerOffset; + try { + brokerOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, i); + } catch (ConsumeQueueException e) { + throw new RemotingCommandException("Failed to get max offset", e); + } + if (brokerOffset < 0) { + brokerOffset = 0; + } + long consumerOffset = this.brokerController.getConsumerOffsetManager().queryOffset( + group, + topic, + i); + if (consumerOffset < 0) + consumerOffset = 0; + + offsetWrapper.setBrokerOffset(brokerOffset); + offsetWrapper.setConsumerOffset(consumerOffset); + + long timeOffset = consumerOffset - 1; + if (timeOffset >= 0) { + long lastTimestamp = this.brokerController.getMessageStore().getMessageStoreTimeStamp(topic, i, timeOffset); + if (lastTimestamp > 0) { + offsetWrapper.setLastTimestamp(lastTimestamp); + } + } + consumeStats.getOffsetTable().put(mq, offsetWrapper); + } + double consumeTps = this.brokerController.getBrokerStatsManager().tpsGroupGetNums(group, topic); + consumeTps += consumeStats.getConsumeTps(); + consumeStats.setConsumeTps(consumeTps); + totalDiff += consumeStats.computeTotalDiff(); + totalInflightDiff += consumeStats.computeInflightTotalDiff(); + consumeStatsList.add(consumeStats); + } + subscripTopicConsumeMap.put(group, consumeStatsList); + brokerConsumeStatsList.add(subscripTopicConsumeMap); + } + ConsumeStatsList consumeStats = new ConsumeStatsList(); + consumeStats.setBrokerAddr(brokerController.getBrokerAddr()); + consumeStats.setConsumeStatsList(brokerConsumeStatsList); + consumeStats.setTotalDiff(totalDiff); + consumeStats.setTotalInflightDiff(totalInflightDiff); + response.setBody(consumeStats.encode()); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private HashMap prepareRuntimeInfo() throws RemotingCommandException { + HashMap runtimeInfo = this.brokerController.getMessageStore().getRuntimeInfo(); + + for (BrokerAttachedPlugin brokerAttachedPlugin : brokerController.getBrokerAttachedPlugins()) { + if (brokerAttachedPlugin != null) { + brokerAttachedPlugin.buildRuntimeInfo(runtimeInfo); + } + } + + try { + this.brokerController.getScheduleMessageService().buildRunningStats(runtimeInfo); + } catch (ConsumeQueueException e) { + throw new RemotingCommandException("Failed to get max offset in queue", e); + } + runtimeInfo.put("brokerActive", String.valueOf(this.brokerController.isSpecialServiceRunning())); + runtimeInfo.put("brokerVersionDesc", MQVersion.getVersionDesc(MQVersion.CURRENT_VERSION)); + runtimeInfo.put("brokerVersion", String.valueOf(MQVersion.CURRENT_VERSION)); + + runtimeInfo.put("msgPutTotalYesterdayMorning", + String.valueOf(this.brokerController.getBrokerStats().getMsgPutTotalYesterdayMorning())); + runtimeInfo.put("msgPutTotalTodayMorning", String.valueOf(this.brokerController.getBrokerStats().getMsgPutTotalTodayMorning())); + runtimeInfo.put("msgPutTotalTodayNow", String.valueOf(this.brokerController.getBrokerStats().getMsgPutTotalTodayNow())); + + runtimeInfo.put("msgGetTotalYesterdayMorning", + String.valueOf(this.brokerController.getBrokerStats().getMsgGetTotalYesterdayMorning())); + runtimeInfo.put("msgGetTotalTodayMorning", String.valueOf(this.brokerController.getBrokerStats().getMsgGetTotalTodayMorning())); + runtimeInfo.put("msgGetTotalTodayNow", String.valueOf(this.brokerController.getBrokerStats().getMsgGetTotalTodayNow())); + + 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())); + + runtimeInfo.put("pullThreadPoolQueueSize", String.valueOf(this.brokerController.getPullThreadPoolQueue().size())); + 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("ackThreadPoolQueueSize", String.valueOf(this.brokerController.getAckThreadPoolQueue().size())); + runtimeInfo.put("ackThreadPoolQueueCapacity", + String.valueOf(this.brokerController.getBrokerConfig().getAckThreadPoolQueueCapacity())); + + runtimeInfo.put("sendThreadPoolQueueHeadWaitTimeMills", String.valueOf(this.brokerController.headSlowTimeMills4SendThreadPoolQueue())); + runtimeInfo.put("pullThreadPoolQueueHeadWaitTimeMills", String.valueOf(brokerController.headSlowTimeMills4PullThreadPoolQueue())); + runtimeInfo.put("queryThreadPoolQueueHeadWaitTimeMills", String.valueOf(this.brokerController.headSlowTimeMills4QueryThreadPoolQueue())); + runtimeInfo.put("litePullThreadPoolQueueHeadWaitTimeMills", String.valueOf(brokerController.headSlowTimeMills4LitePullThreadPoolQueue())); + runtimeInfo.put("ackThreadPoolQueueHeadWaitTimeMills", String.valueOf(brokerController.headSlowTimeMills4AckThreadPoolQueue())); + + runtimeInfo.put("EndTransactionQueueSize", String.valueOf(this.brokerController.getEndTransactionThreadPoolQueue().size())); + runtimeInfo.put("EndTransactionThreadPoolQueueCapacity", + String.valueOf(this.brokerController.getBrokerConfig().getEndTransactionPoolQueueCapacity())); + + return runtimeInfo; + } + + private RemotingCommand callConsumer( + final int requestCode, + final RemotingCommand request, + final String consumerGroup, + final String clientId) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + ClientChannelInfo clientChannelInfo = this.brokerController.getConsumerManager().findChannel(consumerGroup, clientId); + + if (null == clientChannelInfo) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(String.format("The Consumer <%s> <%s> not online", consumerGroup, clientId)); + return response; + } + + if (clientChannelInfo.getVersion() < MQVersion.Version.V3_1_8_SNAPSHOT.ordinal()) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(String.format("The Consumer <%s> Version <%s> too low to finish, please upgrade it to V3_1_8_SNAPSHOT", + clientId, + MQVersion.getVersionDesc(clientChannelInfo.getVersion()))); + return response; + } + + try { + RemotingCommand newRequest = RemotingCommand.createRequestCommand(requestCode, null); + newRequest.setExtFields(request.getExtFields()); + newRequest.setBody(request.getBody()); + + return this.brokerController.getBroker2Client().callClient(clientChannelInfo.getChannel(), newRequest); + } catch (RemotingTimeoutException e) { + response.setCode(ResponseCode.CONSUME_MSG_TIMEOUT); + response + .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, UtilAll.exceptionSimpleDesc(e))); + return response; + } + } + + private RemotingCommand queryConsumeQueue(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + QueryConsumeQueueRequestHeader requestHeader = + (QueryConsumeQueueRequestHeader) request.decodeCommandCustomHeader(QueryConsumeQueueRequestHeader.class); + + RemotingCommand response = RemotingCommand.createResponseCommand(null); + + 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; + } + response.setCode(ResponseCode.SUCCESS); + + QueryConsumeQueueResponseBody body = new QueryConsumeQueueResponseBody(); + body.setMaxQueueIndex(consumeQueue.getMaxOffsetInQueue()); + body.setMinQueueIndex(consumeQueue.getMinOffsetInQueue()); + + MessageFilter messageFilter = null; + if (requestHeader.getConsumerGroup() != null) { + SubscriptionData subscriptionData = this.brokerController.getConsumerManager().findSubscriptionData( + requestHeader.getConsumerGroup(), requestHeader.getTopic() + ); + body.setSubscriptionData(subscriptionData); + if (subscriptionData == null) { + body.setFilterData(String.format("%s@%s is not online!", requestHeader.getConsumerGroup(), requestHeader.getTopic())); + } else { + ConsumerFilterData filterData = this.brokerController.getConsumerFilterManager() + .get(requestHeader.getTopic(), requestHeader.getConsumerGroup()); + body.setFilterData(JSON.toJSONString(filterData, true)); + + messageFilter = new ExpressionMessageFilter(subscriptionData, filterData, + this.brokerController.getConsumerFilterManager()); + } + } + + 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<>(); + while (result.hasNext()) { + CqUnit cqUnit = result.next(); + if (cqUnit.getQueueOffset() - requestHeader.getIndex() >= requestHeader.getCount()) { + break; + } + + ConsumeQueueData one = new ConsumeQueueData(); + one.setPhysicOffset(cqUnit.getPos()); + one.setPhysicSize(cqUnit.getSize()); + one.setTagsCode(cqUnit.getTagsCode()); + + if (cqUnit.getCqExtUnit() == null && cqUnit.isTagsCodeValid()) { + queues.add(one); + continue; + } + + 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()); + } + if (messageFilter != null) { + one.setEval(messageFilter.isMatchedByConsumeQueue(cqExtUnit.getTagsCode(), cqExtUnit)); + } + } else { + one.setMsg("Cq extend not exist!addr: " + one.getTagsCode()); + } + + queues.add(one); + } + body.setQueueData(queues); + } 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().selectTopicConfig(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 RemotingCommand createUser(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + RemotingCommand response = RemotingCommand.createResponseCommand(null); + + CreateUserRequestHeader requestHeader = request.decodeCommandCustomHeader(CreateUserRequestHeader.class); + if (StringUtils.isEmpty(requestHeader.getUsername())) { + response.setCode(ResponseCode.INVALID_PARAMETER); + response.setRemark("The username is blank"); + return response; + } + + UserInfo userInfo = RemotingSerializable.decode(request.getBody(), UserInfo.class); + userInfo.setUsername(requestHeader.getUsername()); + User user = UserConverter.convertUser(userInfo); + + if (user.getUserType() == UserType.SUPER && isNotSuperUserLogin(request)) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("The super user can only be create by super user"); + return response; + } + + this.brokerController.getAuthenticationMetadataManager().createUser(user) + .thenAccept(nil -> response.setCode(ResponseCode.SUCCESS)) + .exceptionally(ex -> { + LOGGER.error("create user {} error", user.getUsername(), ex); + return handleAuthException(response, ex); + }) + .join(); + + return response; + } + + private RemotingCommand updateUser(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + RemotingCommand response = RemotingCommand.createResponseCommand(null); + + UpdateUserRequestHeader requestHeader = request.decodeCommandCustomHeader(UpdateUserRequestHeader.class); + if (StringUtils.isEmpty(requestHeader.getUsername())) { + response.setCode(ResponseCode.INVALID_PARAMETER); + response.setRemark("The username is blank"); + return response; + } + + UserInfo userInfo = RemotingSerializable.decode(request.getBody(), UserInfo.class); + userInfo.setUsername(requestHeader.getUsername()); + User user = UserConverter.convertUser(userInfo); + + if (user.getUserType() == UserType.SUPER && isNotSuperUserLogin(request)) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("The super user can only be update by super user"); + return response; + } + + this.brokerController.getAuthenticationMetadataManager().getUser(requestHeader.getUsername()) + .thenCompose(old -> { + if (old == null) { + throw new AuthenticationException("The user is not exist"); + } + if (old.getUserType() == UserType.SUPER && isNotSuperUserLogin(request)) { + throw new AuthenticationException("The super user can only be update by super user"); + } + return this.brokerController.getAuthenticationMetadataManager().updateUser(user); + }).thenAccept(nil -> response.setCode(ResponseCode.SUCCESS)) + .exceptionally(ex -> { + LOGGER.error("update user {} error", requestHeader.getUsername(), ex); + return handleAuthException(response, ex); + }) + .join(); + return response; + } + + private RemotingCommand deleteUser(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + DeleteUserRequestHeader requestHeader = request.decodeCommandCustomHeader(DeleteUserRequestHeader.class); + + this.brokerController.getAuthenticationMetadataManager().getUser(requestHeader.getUsername()) + .thenCompose(user -> { + if (user == null) { + return CompletableFuture.completedFuture(null); + } + if (user.getUserType() == UserType.SUPER && isNotSuperUserLogin(request)) { + throw new AuthenticationException("The super user can only be update by super user"); + } + return this.brokerController.getAuthenticationMetadataManager().deleteUser(requestHeader.getUsername()); + }).thenAccept(nil -> response.setCode(ResponseCode.SUCCESS)) + .exceptionally(ex -> { + LOGGER.error("delete user {} error", requestHeader.getUsername(), ex); + return handleAuthException(response, ex); + }) + .join(); + return response; + } + + private RemotingCommand getUser(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + GetUserRequestHeader requestHeader = request.decodeCommandCustomHeader(GetUserRequestHeader.class); + + if (StringUtils.isBlank(requestHeader.getUsername())) { + response.setCode(ResponseCode.INVALID_PARAMETER); + response.setRemark("The username is blank"); + return response; + } + + this.brokerController.getAuthenticationMetadataManager().getUser(requestHeader.getUsername()) + .thenAccept(user -> { + response.setCode(ResponseCode.SUCCESS); + if (user != null) { + UserInfo userInfo = UserConverter.convertUser(user); + response.setBody(JSON.toJSONString(userInfo).getBytes(StandardCharsets.UTF_8)); + } + }) + .exceptionally(ex -> { + LOGGER.error("get user {} error", requestHeader.getUsername(), ex); + return handleAuthException(response, ex); + }) + .join(); + + return response; + } + + private RemotingCommand listUser(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + ListUsersRequestHeader requestHeader = request.decodeCommandCustomHeader(ListUsersRequestHeader.class); + + this.brokerController.getAuthenticationMetadataManager().listUser(requestHeader.getFilter()) + .thenAccept(users -> { + response.setCode(ResponseCode.SUCCESS); + if (CollectionUtils.isNotEmpty(users)) { + List userInfos = UserConverter.convertUsers(users); + response.setBody(JSON.toJSONString(userInfos).getBytes(StandardCharsets.UTF_8)); + } + }) + .exceptionally(ex -> { + LOGGER.error("list user by {} error", requestHeader.getFilter(), ex); + return handleAuthException(response, ex); + }) + .join(); + + return response; + } + + private RemotingCommand createAcl(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + RemotingCommand response = RemotingCommand.createResponseCommand(null); + + CreateAclRequestHeader requestHeader = request.decodeCommandCustomHeader(CreateAclRequestHeader.class); + Subject subject = Subject.of(requestHeader.getSubject()); + + AclInfo aclInfo = RemotingSerializable.decode(request.getBody(), AclInfo.class); + if (aclInfo == null || CollectionUtils.isEmpty(aclInfo.getPolicies())) { + throw new AuthorizationException("The body of acl is null"); + } + + Acl acl = AclConverter.convertAcl(aclInfo); + if (acl != null && acl.getSubject() == null) { + acl.setSubject(subject); + } + + this.brokerController.getAuthorizationMetadataManager().createAcl(acl) + .thenAccept(nil -> response.setCode(ResponseCode.SUCCESS)) + .exceptionally(ex -> { + LOGGER.error("create acl for {} error", requestHeader.getSubject(), ex); + return handleAuthException(response, ex); + }) + .join(); + return response; + } + + private RemotingCommand updateAcl(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + RemotingCommand response = RemotingCommand.createResponseCommand(null); + + UpdateAclRequestHeader requestHeader = request.decodeCommandCustomHeader(UpdateAclRequestHeader.class); + Subject subject = Subject.of(requestHeader.getSubject()); + + AclInfo aclInfo = RemotingSerializable.decode(request.getBody(), AclInfo.class); + if (aclInfo == null || CollectionUtils.isEmpty(aclInfo.getPolicies())) { + throw new AuthorizationException("The body of acl is null"); + } + + Acl acl = AclConverter.convertAcl(aclInfo); + if (acl != null && acl.getSubject() == null) { + acl.setSubject(subject); + } + + this.brokerController.getAuthorizationMetadataManager().updateAcl(acl) + .thenAccept(nil -> response.setCode(ResponseCode.SUCCESS)) + .exceptionally(ex -> { + LOGGER.error("update acl for {} error", requestHeader.getSubject(), ex); + return handleAuthException(response, ex); + }) + .join(); + + return response; + } + + private RemotingCommand deleteAcl(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + DeleteAclRequestHeader requestHeader = request.decodeCommandCustomHeader(DeleteAclRequestHeader.class); + + Subject subject = Subject.of(requestHeader.getSubject()); + + PolicyType policyType = PolicyType.getByName(requestHeader.getPolicyType()); + + Resource resource = Resource.of(requestHeader.getResource()); + + this.brokerController.getAuthorizationMetadataManager().deleteAcl(subject, policyType, resource) + .thenAccept(nil -> { + response.setCode(ResponseCode.SUCCESS); + }) + .exceptionally(ex -> { + LOGGER.error("delete acl for {} error", requestHeader.getSubject(), ex); + return handleAuthException(response, ex); + }) + .join(); + + return response; + } + + private RemotingCommand getAcl(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + GetAclRequestHeader requestHeader = request.decodeCommandCustomHeader(GetAclRequestHeader.class); + + Subject subject = Subject.of(requestHeader.getSubject()); + + this.brokerController.getAuthorizationMetadataManager().getAcl(subject) + .thenAccept(acl -> { + response.setCode(ResponseCode.SUCCESS); + if (acl != null) { + AclInfo aclInfo = AclConverter.convertAcl(acl); + String body = JSON.toJSONString(aclInfo); + response.setBody(body.getBytes(StandardCharsets.UTF_8)); + } + }) + .exceptionally(ex -> { + LOGGER.error("get acl for {} error", requestHeader.getSubject(), ex); + return handleAuthException(response, ex); + }) + .join(); + + return response; + } + + private RemotingCommand listAcl(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + ListAclsRequestHeader requestHeader = request.decodeCommandCustomHeader(ListAclsRequestHeader.class); + + this.brokerController.getAuthorizationMetadataManager() + .listAcl(requestHeader.getSubjectFilter(), requestHeader.getResourceFilter()) + .thenAccept(acls -> { + response.setCode(ResponseCode.SUCCESS); + if (CollectionUtils.isNotEmpty(acls)) { + List aclInfos = AclConverter.convertAcls(acls); + String body = JSON.toJSONString(aclInfos); + response.setBody(body.getBytes(StandardCharsets.UTF_8)); + } + }) + .exceptionally(ex -> { + LOGGER.error("list acl error, subjectFilter:{}, resourceFilter:{}", requestHeader.getSubjectFilter(), requestHeader.getResourceFilter(), ex); + return handleAuthException(response, ex); + }) + .join(); + + return response; + } + + private boolean isNotSuperUserLogin(RemotingCommand request) { + String accessKey = request.getExtFields().get("AccessKey"); + // if accessKey is null, it may be authentication is not enabled. + if (StringUtils.isEmpty(accessKey)) { + return false; + } + return !this.brokerController.getAuthenticationMetadataManager() + .isSuperUser(accessKey).join(); + } + + private Void handleAuthException(RemotingCommand response, Throwable ex) { + Throwable throwable = ExceptionUtils.getRealException(ex); + if (throwable instanceof AuthenticationException || throwable instanceof AuthorizationException) { + response.setCode(ResponseCode.NO_PERMISSION); + response.setRemark(throwable.getMessage()); + } else { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("An system error occurred, please try again later."); + LOGGER.error("An system error occurred when processing auth admin request.", ex); + } + return null; + } + + 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; + } + + private CheckRocksdbCqWriteResult doCheckRocksdbCqWriteProgress(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + CheckRocksdbCqWriteProgressRequestHeader requestHeader = request.decodeCommandCustomHeader(CheckRocksdbCqWriteProgressRequestHeader.class); + String requestTopic = requestHeader.getTopic(); + MessageStore messageStore = brokerController.getMessageStore(); + DefaultMessageStore defaultMessageStore; + if (messageStore instanceof AbstractPluginMessageStore) { + defaultMessageStore = (DefaultMessageStore) ((AbstractPluginMessageStore) messageStore).getNext(); + } else { + defaultMessageStore = (DefaultMessageStore) messageStore; + } + RocksDBMessageStore rocksDBMessageStore = defaultMessageStore.getRocksDBMessageStore(); + CheckRocksdbCqWriteResult result = new CheckRocksdbCqWriteResult(); + + if (defaultMessageStore.getMessageStoreConfig().getStoreType().equals(StoreType.DEFAULT_ROCKSDB.getStoreType())) { + result.setCheckResult("storeType is DEFAULT_ROCKSDB, no need check"); + result.setCheckStatus(CheckRocksdbCqWriteResult.CheckStatus.CHECK_OK.getValue()); + return result; + } + + if (!defaultMessageStore.getMessageStoreConfig().isRocksdbCQDoubleWriteEnable()) { + result.setCheckResult("rocksdbCQWriteEnable is false, checkRocksdbCqWriteProgressCommand is invalid"); + result.setCheckStatus(CheckRocksdbCqWriteResult.CheckStatus.CHECK_NOT_OK.getValue()); + return result; + } + + ConcurrentMap> cqTable = defaultMessageStore.getConsumeQueueTable(); + StringBuilder diffResult = new StringBuilder(); + try { + if (StringUtils.isNotBlank(requestTopic)) { + boolean checkResult = processConsumeQueuesForTopic(cqTable.get(requestTopic), requestTopic, rocksDBMessageStore, diffResult, true, requestHeader.getCheckStoreTime()); + result.setCheckResult(diffResult.toString()); + result.setCheckStatus(checkResult ? CheckRocksdbCqWriteResult.CheckStatus.CHECK_OK.getValue() : CheckRocksdbCqWriteResult.CheckStatus.CHECK_NOT_OK.getValue()); + return result; + } + int successNum = 0; + int checkSize = 0; + for (Map.Entry> topicEntry : cqTable.entrySet()) { + boolean checkResult = processConsumeQueuesForTopic(topicEntry.getValue(), topicEntry.getKey(), rocksDBMessageStore, diffResult, false, requestHeader.getCheckStoreTime()); + successNum += checkResult ? 1 : 0; + checkSize++; + } + // check all topic finish, all topic is ready, checkSize: 100, currentQueueNum: 110 -> ready (The currentQueueNum means when we do checking, new topics are added.) + // check all topic finish, success/all : 89/100, currentQueueNum: 110 -> not ready + boolean checkReady = successNum == checkSize; + String checkResultString = checkReady ? String.format("all topic is ready, checkSize: %s, currentQueueNum: %s", checkSize, cqTable.size()) : + String.format("success/all : %s/%s, currentQueueNum: %s", successNum, checkSize, cqTable.size()); + diffResult.append("check all topic finish, ").append(checkResultString); + result.setCheckResult(diffResult.toString()); + result.setCheckStatus(checkReady ? CheckRocksdbCqWriteResult.CheckStatus.CHECK_OK.getValue() : CheckRocksdbCqWriteResult.CheckStatus.CHECK_NOT_OK.getValue()); + } catch (Exception e) { + LOGGER.error("CheckRocksdbCqWriteProgressCommand error", e); + result.setCheckResult(e.getMessage() + Arrays.toString(e.getStackTrace())); + result.setCheckStatus(CheckRocksdbCqWriteResult.CheckStatus.CHECK_ERROR.getValue()); + } + return result; + } + + private boolean processConsumeQueuesForTopic(ConcurrentMap queueMap, String topic, + RocksDBMessageStore rocksDBMessageStore, StringBuilder diffResult, boolean printDetail, + long checkpointByStoreTime) { + boolean processResult = true; + for (Map.Entry queueEntry : queueMap.entrySet()) { + Integer queueId = queueEntry.getKey(); + ConsumeQueueInterface jsonCq = queueEntry.getValue(); + ConsumeQueueInterface kvCq = rocksDBMessageStore.getConsumeQueue(topic, queueId); + if (printDetail) { + String format = String.format("[topic: %s, queue: %s] \n kvEarliest : %s | kvLatest : %s \n fileEarliest: %s | fileEarliest: %s ", + topic, queueId, kvCq.getEarliestUnit(), kvCq.getLatestUnit(), jsonCq.getEarliestUnit(), jsonCq.getLatestUnit()); + diffResult.append(format).append("\n"); + } + + long minOffsetByTime = 0L; + try { + minOffsetByTime = rocksDBMessageStore.getConsumeQueueStore().getOffsetInQueueByTime(topic, queueId, checkpointByStoreTime, BoundaryType.UPPER); + } catch (Exception e) { + // ignore + } + long minOffsetInQueue = kvCq.getMinOffsetInQueue(); + long checkFrom = Math.max(minOffsetInQueue, minOffsetByTime); + long checkTo = jsonCq.getMaxOffsetInQueue() - 1; + /* + checkTo(maxOffsetInQueue - 1) + v + fileCq +------------------------------------------------------+ + kvCq +----------------------------------------------+ + ^ ^ + minOffsetInQueue minOffsetByTime + ^ + checkFrom = max(minOffsetInQueue, minOffsetByTime) + */ + // The latest message is earlier than the check time + Pair fileLatestCq = jsonCq.getCqUnitAndStoreTime(checkTo); + if (fileLatestCq != null) { + if (fileLatestCq.getObject2() < checkpointByStoreTime) { + continue; + } + } + for (long i = checkFrom; i <= checkTo; i++) { + Pair fileCqUnit = jsonCq.getCqUnitAndStoreTime(i); + Pair kvCqUnit = kvCq.getCqUnitAndStoreTime(i); + if (fileCqUnit == null || kvCqUnit == null || !checkCqUnitEqual(kvCqUnit.getObject1(), fileCqUnit.getObject1())) { + LOGGER.error(String.format("[topic: %s, queue: %s, offset: %s] \n file : %s \n kv : %s \n", + topic, queueId, i, kvCqUnit != null ? kvCqUnit.getObject1() : "null", fileCqUnit != null ? fileCqUnit.getObject1() : "null")); + processResult = false; + break; + } + } + } + return processResult; + } + + private boolean checkCqUnitEqual(CqUnit cqUnit1, CqUnit cqUnit2) { + if (cqUnit1.getQueueOffset() != cqUnit2.getQueueOffset()) { + return false; + } + if (cqUnit1.getSize() != cqUnit2.getSize()) { + return false; + } + if (cqUnit1.getPos() != cqUnit2.getPos()) { + return false; + } + if (cqUnit1.getBatchNum() != cqUnit2.getBatchNum()) { + return false; + } + return cqUnit1.getTagsCode() == cqUnit2.getTagsCode(); + } + + private RemotingCommand transferPopToFsStore(ChannelHandlerContext ctx, RemotingCommand request) { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + try { + if (brokerController.getPopConsumerService() != null) { + brokerController.getPopConsumerService().transferToFsStore(); + } + response.setCode(ResponseCode.SUCCESS); + } catch (Exception e) { + LOGGER.error("PopConsumerStore transfer from kvStore to fsStore finish [{}]", request, e); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(e.getMessage()); + } + return response; + } +} 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 0000000..de72ee7 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java @@ -0,0 +1,362 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.nio.charset.StandardCharsets; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.metrics.PopMetricsManager; +import org.apache.rocketmq.broker.offset.ConsumerOffsetManager; +import org.apache.rocketmq.broker.offset.ConsumerOrderInfoManager; +import org.apache.rocketmq.broker.pop.PopConsumerLockService; +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.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.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.PutMessageStatus; +import org.apache.rocketmq.store.exception.ConsumeQueueException; +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 { + + CompletableFuture responseFuture = processRequestAsync(channel, request, brokerAllowSuspend); + + if (brokerController.getBrokerConfig().isAppendCkAsync() && brokerController.getBrokerConfig().isAppendAckAsync()) { + responseFuture.thenAccept(response -> doResponse(channel, request, response)).exceptionally(throwable -> { + RemotingCommand response = RemotingCommand.createResponseCommand(ChangeInvisibleTimeResponseHeader.class); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setOpaque(request.getOpaque()); + doResponse(channel, request, response); + POP_LOGGER.error("append checkpoint or ack origin failed", throwable); + return null; + }); + } else { + RemotingCommand response; + try { + response = responseFuture.get(3000, TimeUnit.MILLISECONDS); + } catch (Exception e) { + response = RemotingCommand.createResponseCommand(ChangeInvisibleTimeResponseHeader.class); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setOpaque(request.getOpaque()); + POP_LOGGER.error("append checkpoint or ack origin failed", e); + } + return response; + } + return null; + } + + public CompletableFuture processRequestAsync(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 CompletableFuture.completedFuture(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 CompletableFuture.completedFuture(response); + } + long minOffset = this.brokerController.getMessageStore().getMinOffsetInQueue(requestHeader.getTopic(), requestHeader.getQueueId()); + long maxOffset; + try { + maxOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(requestHeader.getTopic(), requestHeader.getQueueId()); + } catch (ConsumeQueueException e) { + throw new RemotingCommandException("Failed to get max consume offset", e); + } + if (requestHeader.getOffset() < minOffset || requestHeader.getOffset() >= maxOffset) { + response.setCode(ResponseCode.NO_MESSAGE); + return CompletableFuture.completedFuture(response); + } + + String[] extraInfo = ExtraInfoUtil.split(requestHeader.getExtraInfo()); + if (brokerController.getBrokerConfig().isPopConsumerKVServiceEnable()) { + if (ExtraInfoUtil.isOrder(extraInfo)) { + return this.processChangeInvisibleTimeForOrderNew( + requestHeader, extraInfo, response, responseHeader); + } + try { + long current = System.currentTimeMillis(); + brokerController.getPopConsumerService().changeInvisibilityDuration( + ExtraInfoUtil.getPopTime(extraInfo), ExtraInfoUtil.getInvisibleTime(extraInfo), current, + requestHeader.getInvisibleTime(), requestHeader.getConsumerGroup(), requestHeader.getTopic(), + requestHeader.getQueueId(), requestHeader.getOffset()); + responseHeader.setInvisibleTime(requestHeader.getInvisibleTime()); + responseHeader.setPopTime(current); + responseHeader.setReviveQid(ExtraInfoUtil.getReviveQid(extraInfo)); + } catch (Exception e) { + response.setCode(ResponseCode.SYSTEM_ERROR); + } + return CompletableFuture.completedFuture(response); + } + + if (ExtraInfoUtil.isOrder(extraInfo)) { + return CompletableFuture.completedFuture( + processChangeInvisibleTimeForOrder(requestHeader, extraInfo, response, responseHeader)); + } + + // add new ck + long now = System.currentTimeMillis(); + CompletableFuture futureResult = appendCheckPointThenAckOrigin(requestHeader, + ExtraInfoUtil.getReviveQid(extraInfo), requestHeader.getQueueId(), requestHeader.getOffset(), now, extraInfo); + + return futureResult.thenCompose(result -> { + if (result) { + responseHeader.setInvisibleTime(requestHeader.getInvisibleTime()); + responseHeader.setPopTime(now); + responseHeader.setReviveQid(ExtraInfoUtil.getReviveQid(extraInfo)); + } else { + response.setCode(ResponseCode.SYSTEM_ERROR); + } + return CompletableFuture.completedFuture(response); + }); + } + + @SuppressWarnings({"StatementWithEmptyBody", "DuplicatedCode"}) + public CompletableFuture processChangeInvisibleTimeForOrderNew( + ChangeInvisibleTimeRequestHeader requestHeader, String[] extraInfo, + RemotingCommand response, ChangeInvisibleTimeResponseHeader responseHeader) { + + String groupId = requestHeader.getConsumerGroup(); + String topicId = requestHeader.getTopic(); + Integer queueId = requestHeader.getQueueId(); + long popTime = ExtraInfoUtil.getPopTime(extraInfo); + + PopConsumerLockService consumerLockService = + this.brokerController.getPopConsumerService().getConsumerLockService(); + ConsumerOffsetManager consumerOffsetManager = this.brokerController.getConsumerOffsetManager(); + ConsumerOrderInfoManager consumerOrderInfoManager = brokerController.getConsumerOrderInfoManager(); + + long oldOffset = consumerOffsetManager.queryOffset(groupId, topicId, queueId); + if (requestHeader.getOffset() < oldOffset) { + return CompletableFuture.completedFuture(response); + } + + while (!consumerLockService.tryLock(groupId, topicId)) { + } + + try { + // double check + oldOffset = consumerOffsetManager.queryOffset(groupId, topicId, queueId); + if (requestHeader.getOffset() < oldOffset) { + return CompletableFuture.completedFuture(response); + } + + long visibilityTimeout = System.currentTimeMillis() + requestHeader.getInvisibleTime(); + consumerOrderInfoManager.updateNextVisibleTime( + topicId, groupId, queueId, requestHeader.getOffset(), popTime, visibilityTimeout); + + responseHeader.setInvisibleTime(visibilityTimeout - popTime); + responseHeader.setPopTime(popTime); + responseHeader.setReviveQid(ExtraInfoUtil.getReviveQid(extraInfo)); + } finally { + consumerLockService.unlock(groupId, topicId); + } + + return CompletableFuture.completedFuture(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 CompletableFuture 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 CompletableFuture.completedFuture(true); + } + + msgInner.setTopic(reviveTopic); + msgInner.setBody(JSON.toJSONString(ackMsg).getBytes(StandardCharsets.UTF_8)); + 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())); + return this.brokerController.getEscapeBridge().asyncPutMessageToSpecificQueue(msgInner).thenCompose(putMessageResult -> { + 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()); + return CompletableFuture.completedFuture(true); + }).exceptionally(e -> { + POP_LOGGER.error("change Invisible, put ack msg error: {}, {}", requestHeader.getExtraInfo(), e.getMessage()); + return false; + }); + } + + private CompletableFuture appendCheckPointThenAckOrigin( + final ChangeInvisibleTimeRequestHeader requestHeader, + int reviveQid, + int queueId, long offset, long popTime, String[] extraInfo) { + // 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(ExtraInfoUtil.getBrokerName(extraInfo)); + + msgInner.setBody(JSON.toJSONString(ck).getBytes(StandardCharsets.UTF_8)); + 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())); + return this.brokerController.getEscapeBridge().asyncPutMessageToSpecificQueue(msgInner).thenCompose(putMessageResult -> { + 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); + } + } + 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 new ck error: {}", putMessageResult); + return CompletableFuture.completedFuture(false); + } else { + return ackOrigin(requestHeader, extraInfo); + } + }).exceptionally(throwable -> { + POP_LOGGER.error("change invisible, put new ck error", throwable); + return null; + }); + } + + protected void doResponse(Channel channel, RemotingCommand request, + final RemotingCommand response) { + NettyRemotingAbstract.writeResponse(channel, request, response); + } +} 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 new file mode 100644 index 0000000..b51967e --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/ClientManageProcessor.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.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; +import org.apache.rocketmq.common.MixAll; +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.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.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 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; + } + + @Override + public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) + throws RemotingCommandException { + switch (request.getCode()) { + case RequestCode.HEART_BEAT: + return this.heartBeat(ctx, request); + case RequestCode.UNREGISTER_CLIENT: + return this.unregisterClient(ctx, request); + case RequestCode.CHECK_CLIENT_CONFIG: + return this.checkClientConfig(ctx, request); + default: + break; + } + return null; + } + + @Override + public boolean rejectRequest() { + return false; + } + + public RemotingCommand heartBeat(ChannelHandlerContext ctx, RemotingCommand request) { + RemotingCommand response = RemotingCommand.createResponseCommand(null); + HeartbeatData heartbeatData = HeartbeatData.decode(request.getBody(), HeartbeatData.class); + ClientChannelInfo clientChannelInfo = new ClientChannelInfo( + ctx.channel(), + heartbeatData.getClientID(), + 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 (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 = this.brokerController.getConsumerManager().registerConsumer( + consumerData.getGroupName(), + clientChannelInfo, + consumerData.getConsumeType(), + consumerData.getMessageModel(), + consumerData.getConsumeFromWhere(), + consumerData.getSubscriptionDataSet(), + isNotifyConsumerIdsChangedEnable + ); + if (changed) { + LOGGER.info("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.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; + } + + public RemotingCommand unregisterClient(ChannelHandlerContext ctx, RemotingCommand request) + throws RemotingCommandException { + final RemotingCommand response = + RemotingCommand.createResponseCommand(UnregisterClientResponseHeader.class); + final UnregisterClientRequestHeader requestHeader = + (UnregisterClientRequestHeader) request + .decodeCommandCustomHeader(UnregisterClientRequestHeader.class); + + ClientChannelInfo clientChannelInfo = new ClientChannelInfo( + ctx.channel(), + requestHeader.getClientID(), + request.getLanguage(), + request.getVersion()); + { + final String group = requestHeader.getProducerGroup(); + if (group != null) { + this.brokerController.getProducerManager().unregisterProducer(group, clientChannelInfo); + } + } + + { + final String group = requestHeader.getConsumerGroup(); + if (group != null) { + SubscriptionGroupConfig subscriptionGroupConfig = + this.brokerController.getSubscriptionGroupManager().findSubscriptionGroupConfig(group); + boolean isNotifyConsumerIdsChangedEnable = true; + if (null != subscriptionGroupConfig) { + isNotifyConsumerIdsChangedEnable = subscriptionGroupConfig.isNotifyConsumerIdsChangedEnable(); + } + this.brokerController.getConsumerManager().unregisterConsumer(group, clientChannelInfo, isNotifyConsumerIdsChangedEnable); + } + } + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + public RemotingCommand checkClientConfig(ChannelHandlerContext ctx, RemotingCommand request) + throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + CheckClientRequestBody requestBody = CheckClientRequestBody.decode(request.getBody(), + CheckClientRequestBody.class); + + if (requestBody != null && requestBody.getSubscriptionData() != null) { + SubscriptionData subscriptionData = requestBody.getSubscriptionData(); + + if (ExpressionType.isTagType(subscriptionData.getExpressionType())) { + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + if (!this.brokerController.getBrokerConfig().isEnablePropertyFilter()) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("The broker does not support consumer to filter message by " + subscriptionData.getExpressionType()); + return response; + } + + try { + FilterFactory.INSTANCE.get(subscriptionData.getExpressionType()).compile(subscriptionData.getSubString()); + } catch (Exception e) { + 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()); + return response; + } + } + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } +} 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 new file mode 100644 index 0000000..dfa755d --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/ConsumerManageProcessor.java @@ -0,0 +1,355 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.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.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; + +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) { + this.brokerController = brokerController; + } + + @Override + public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) + throws RemotingCommandException { + switch (request.getCode()) { + case RequestCode.GET_CONSUMER_LIST_BY_GROUP: + return this.getConsumerListByGroup(ctx, request); + case RequestCode.UPDATE_CONSUMER_OFFSET: + return this.updateConsumerOffset(ctx, request); + case RequestCode.QUERY_CONSUMER_OFFSET: + return this.queryConsumerOffset(ctx, request); + default: + break; + } + return null; + } + + @Override + public boolean rejectRequest() { + return false; + } + + public RemotingCommand getConsumerListByGroup(ChannelHandlerContext ctx, RemotingCommand request) + throws RemotingCommandException { + final RemotingCommand response = + RemotingCommand.createResponseCommand(GetConsumerListByGroupResponseHeader.class); + final GetConsumerListByGroupRequestHeader requestHeader = + (GetConsumerListByGroupRequestHeader) request + .decodeCommandCustomHeader(GetConsumerListByGroupRequestHeader.class); + + ConsumerGroupInfo consumerGroupInfo = + this.brokerController.getConsumerManager().getConsumerGroupInfo( + requestHeader.getConsumerGroup()); + if (consumerGroupInfo != null) { + List clientIds = consumerGroupInfo.getAllClientId(); + if (!clientIds.isEmpty()) { + GetConsumerListByGroupResponseBody body = new GetConsumerListByGroupResponseBody(); + body.setConsumerIdList(clientIds); + response.setBody(body.encode()); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } else { + LOGGER.warn("getAllClientId failed, {} {}", requestHeader.getConsumerGroup(), + RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + } + } else { + LOGGER.warn("getConsumerGroupInfo failed, {} {}", requestHeader.getConsumerGroup(), + RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + } + + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("no consumer for this group, " + requestHeader.getConsumerGroup()); + 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); + + 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.getSubscriptionGroupManager().containsSubscriptionGroup(group)) { + response.setCode(ResponseCode.SUBSCRIPTION_GROUP_NOT_EXIST); + response.setRemark("Group " + group + " not exist!"); + return response; + } + + 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.INVALID_PARAMETER); + response.setRemark("QueueId is null, topic is " + topic); + return response; + } + + if (offset == null) { + response.setCode(ResponseCode.INVALID_PARAMETER); + 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 = + RemotingCommand.createResponseCommand(QueryConsumerOffsetResponseHeader.class); + final QueryConsumerOffsetResponseHeader responseHeader = + (QueryConsumerOffsetResponseHeader) response.readCustomHeader(); + final QueryConsumerOffsetRequestHeader requestHeader = + (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()); + + if (offset >= 0) { + responseHeader.setOffset(offset); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + } else { + long minOffset = + this.brokerController.getMessageStore().getMinOffsetInQueue(requestHeader.getTopic(), + requestHeader.getQueueId()); + 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); + } else { + response.setCode(ResponseCode.QUERY_NOT_FOUND); + response.setRemark("Not found, V3_0_6_SNAPSHOT maybe this group consumer boot first"); + } + } + + 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 0000000..43b66b4 --- /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 new file mode 100644 index 0000000..468a879 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/EndTransactionProcessor.java @@ -0,0 +1,349 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.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.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.EndTransactionRequestHeader; +import org.apache.rocketmq.store.PutMessageResult; +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; + + public EndTransactionProcessor(final BrokerController brokerController) { + this.brokerController = brokerController; + } + + @Override + 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." + + "RequestHeader: {} Remark: {}", + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), + requestHeader.toString(), + request.getRemark()); + return null; + } + + case MessageSysFlag.TRANSACTION_COMMIT_TYPE: { + LOGGER.warn("Check producer[{}] transaction state, the producer commit the message." + + "RequestHeader: {} Remark: {}", + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), + requestHeader.toString(), + request.getRemark()); + + break; + } + + case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE: { + LOGGER.warn("Check producer[{}] transaction state, the producer rollback the message." + + "RequestHeader: {} Remark: {}", + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), + requestHeader.toString(), + request.getRemark()); + break; + } + default: + return null; + } + } else { + switch (requestHeader.getCommitOrRollback()) { + case MessageSysFlag.TRANSACTION_NOT_TYPE: { + LOGGER.warn("The producer[{}] end transaction in sending message, and it's pending status." + + "RequestHeader: {} Remark: {}", + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), + requestHeader.toString(), + request.getRemark()); + return null; + } + + case MessageSysFlag.TRANSACTION_COMMIT_TYPE: { + break; + } + + case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE: { + LOGGER.warn("The producer[{}] end transaction in sending message, rollback the message." + + "RequestHeader: {} Remark: {}", + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), + requestHeader.toString(), + request.getRemark()); + break; + } + default: + 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(); + + 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"); + return response; + } + + if (msgExt.getQueueOffset() != requestHeader.getTranStateTableOffset()) { + response.setCode(ResponseCode.SYSTEM_ERROR); + 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; + } + } else { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("Find prepared transaction message failed"); + return response; + } + response.setCode(ResponseCode.SUCCESS); + return response; + } + + 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()); + 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())); + 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: + this.brokerController.getBrokerStatsManager().incTopicPutNums(msgInner.getTopic(), putMessageResult.getAppendMessageResult().getMsgNum(), 1); + this.brokerController.getBrokerStatsManager().incTopicPutSize(msgInner.getTopic(), putMessageResult.getAppendMessageResult().getWroteBytes()); + this.brokerController.getBrokerStatsManager().incBrokerPutNums(msgInner.getTopic(), putMessageResult.getAppendMessageResult().getMsgNum()); + 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/NotificationProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/NotificationProcessor.java new file mode 100644 index 0000000..2fe3464 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/NotificationProcessor.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.broker.processor; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import java.util.Map; +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; +import org.apache.rocketmq.store.exception.ConsumeQueueException; + +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, true); + } + + @Override + public boolean rejectRequest() { + return false; + } + + // When a new message is written to CommitLog, this method would be called. + // Suspended long polling will receive notification and be wakeup. + public void notifyMessageArriving(final String topic, final int queueId, long offset, + Long tagsCode, long msgStoreTime, byte[] filterBitMap, Map properties) { + this.popLongPollingService.notifyMessageArrivingWithRetryTopic( + topic, queueId, offset, tagsCode, msgStoreTime, filterBitMap, properties); + } + + public void notifyMessageArriving(final String topic, final int queueId) { + this.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.INVALID_PARAMETER); + 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) { + PollingResult pollingResult = popLongPollingService.polling(ctx, request, new PollingHeader(requestHeader)); + if (pollingResult == PollingResult.POLLING_SUC) { + return null; + } else if (pollingResult == PollingResult.POLLING_FULL) { + responseHeader.setPollingFull(true); + } + } + response.setCode(ResponseCode.SUCCESS); + responseHeader.setHasMsg(hasMsg); + return response; + } + + private boolean hasMsgFromTopic(String topicName, int randomQ, NotificationRequestHeader requestHeader) + throws RemotingCommandException { + TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(topicName); + return hasMsgFromTopic(topicConfig, randomQ, requestHeader); + } + + private boolean hasMsgFromTopic(TopicConfig topicConfig, int randomQ, NotificationRequestHeader requestHeader) + throws RemotingCommandException { + 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) throws RemotingCommandException { + 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); + try { + long restNum = this.brokerController.getMessageStore().getMaxOffsetInQueue(targetTopic, queueId) - offset; + return restNum > 0; + } catch (ConsumeQueueException e) { + throw new RemotingCommandException("Failed tp get max offset in queue", e); + } + } + + 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; + if (brokerController.getBrokerConfig().isPopConsumerKVServiceEnable()) { + bufferOffset = this.brokerController.getConsumerOffsetManager().queryPullOffset(cid, topic, queueId); + } else { + bufferOffset = this.brokerController.getPopMessageProcessor() + .getPopBufferMergeService().getLatestOffset(topic, cid, queueId); + } + + return bufferOffset < 0L ? offset : Math.max(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 0000000..40117b7 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PeekMessageProcessor.java @@ -0,0 +1,308 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.rocketmq.store.exception.ConsumeQueueException; + +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.INVALID_PARAMETER); + 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) throws RemotingCommandException { + String topic = isRetry ? + KeyBuilder.buildPopRetryTopic(requestHeader.getTopic(), requestHeader.getConsumerGroup(), brokerController.getBrokerConfig().isEnableRetryTopicV2()) + : requestHeader.getTopic(); + GetMessageResult getMessageTmpResult; + long offset = getPopOffset(topic, requestHeader.getConsumerGroup(), queueId); + try { + restNum = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId) - offset + restNum; + } catch (ConsumeQueueException e) { + LOG.error("Failed to get max offset in queue. topic={}, queue-id={}", topic, queueId, e); + throw new RemotingCommandException("Failed to get max offset in queue", e); + } + 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 mappedBuffer : getMessageTmpResult.getMessageMapedList()) { + getMessageResult.addMessage(mappedBuffer); + } + } + 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 0000000..f7baac1 --- /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.INVALID_PARAMETER); + 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 0000000..820388b --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -0,0 +1,927 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 nonexistent topic {} in buffer!", topic); + iterator.remove(); + continue; + } + if (!brokerController.getSubscriptionGroupManager().containsSubscriptionGroup(cid)) { + POP_LOGGER.info("[PopBuffer]remove nonexistent subscription group {} 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(); + AtomicInteger count = new AtomicInteger(0); + int 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, this.brokerController.getBrokerConfig().isAppendCkAsync()); + countCk++; + } + continue; + } else if (removeCk) { + // put buffer ak to store + if (pointWrapper.getReviveQueueOffset() < 0) { + putCkToStore(pointWrapper, this.brokerController.getBrokerConfig().isAppendCkAsync()); + 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) { + putBatchAckToStore(pointWrapper, indexList, count); + } + } 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)) { + putAckToStore(pointWrapper, i, count); + } + } + } + + if (isCkDoneForFinish(pointWrapper) && pointWrapper.isCkStored()) { + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("[PopBuffer]ck finish, {}", pointWrapper); + } + iterator.remove(); + counter.decrementAndGet(); + } + } + } + + 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.get(), countCk, counter.get(), offsetBufferSize); + this.serving = false; + } else { + if (scanTimes % countOfSecond1 == 0) { + POP_LOGGER.info("[PopBuffer]scan, PopBufferEclipse={}, " + + "PopBufferToStoreAck={}, PopBufferToStoreCk={}, PopBufferSize={}, PopBufferOffsetSize={}", + eclipse, count.get(), 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()); + + // Indicates that ck message is storing + pointWrapper.setReviveQueueOffset(Long.MAX_VALUE); + if (brokerController.getBrokerConfig().isAppendCkAsync() && runInCurrent) { + brokerController.getEscapeBridge().asyncPutMessageToSpecificQueue(msgInner).thenAccept(putMessageResult -> { + handleCkMessagePutResult(putMessageResult, pointWrapper); + }).exceptionally(throwable -> { + POP_LOGGER.error("[PopBuffer]put ck to store fail: {}", pointWrapper, throwable); + pointWrapper.setReviveQueueOffset(-1); + return null; + }); + } else { + PutMessageResult putMessageResult = brokerController.getEscapeBridge().putMessageToSpecificQueue(msgInner); + handleCkMessagePutResult(putMessageResult, pointWrapper); + } + } + + private void handleCkMessagePutResult(PutMessageResult putMessageResult, final PopCheckPointWrapper pointWrapper) { + 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) { + pointWrapper.setReviveQueueOffset(-1); + 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 void putAckToStore(final PopCheckPointWrapper pointWrapper, byte msgIndex, AtomicInteger count) { + 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()); + ackMsg.setBrokerName(point.getBrokerName()); + msgInner.setTopic(popMessageProcessor.getReviveTopic()); + 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())); + + if (brokerController.getBrokerConfig().isAppendAckAsync()) { + brokerController.getEscapeBridge().asyncPutMessageToSpecificQueue(msgInner).thenAccept(putMessageResult -> { + handleAckPutMessageResult(ackMsg, putMessageResult, pointWrapper, count, msgIndex); + }).exceptionally(throwable -> { + POP_LOGGER.error("[PopBuffer]put ack to store fail: {}, {}", pointWrapper, ackMsg, throwable); + return null; + }); + } else { + PutMessageResult putMessageResult = brokerController.getEscapeBridge().putMessageToSpecificQueue(msgInner); + handleAckPutMessageResult(ackMsg, putMessageResult, pointWrapper, count, msgIndex); + } + } + + private void handleAckPutMessageResult(AckMsg ackMsg, PutMessageResult putMessageResult, + PopCheckPointWrapper pointWrapper, AtomicInteger count, byte msgIndex) { + 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; + } + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("[PopBuffer]put ack to store ok: {}, {}, {}", pointWrapper, ackMsg, putMessageResult); + } + count.incrementAndGet(); + markBitCAS(pointWrapper.getToStoreBits(), msgIndex); + } + + private void putBatchAckToStore(final PopCheckPointWrapper pointWrapper, final List msgIndexList, + AtomicInteger count) { + 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.getReviveTopic()); + 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())); + if (brokerController.getBrokerConfig().isAppendAckAsync()) { + brokerController.getEscapeBridge().asyncPutMessageToSpecificQueue(msgInner).thenAccept(putMessageResult -> { + handleBatchAckPutMessageResult(batchAckMsg, putMessageResult, pointWrapper, count, msgIndexList); + }).exceptionally(throwable -> { + POP_LOGGER.error("[PopBuffer]put batchAckMsg to store fail: {}, {}", pointWrapper, batchAckMsg, throwable); + return null; + }); + } else { + PutMessageResult putMessageResult = brokerController.getEscapeBridge().putMessageToSpecificQueue(msgInner); + handleBatchAckPutMessageResult(batchAckMsg, putMessageResult, pointWrapper, count, msgIndexList); + } + } + + private void handleBatchAckPutMessageResult(BatchAckMsg batchAckMsg, PutMessageResult putMessageResult, + PopCheckPointWrapper pointWrapper, AtomicInteger count, List msgIndexList) { + 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; + } + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("[PopBuffer]put batch ack to store ok: {}, {}, {}", pointWrapper, batchAckMsg, putMessageResult); + } + + count.addAndGet(msgIndexList.size()); + for (Byte i : msgIndexList) { + markBitCAS(pointWrapper.getToStoreBits(), i); + } + } + + 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.getReviveTopic()); + 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 0000000..6749af3 --- /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 0000000..d73acc8 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java @@ -0,0 +1,1111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.nio.charset.StandardCharsets; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +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.broker.pop.PopConsumerContext; +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.ConcurrentHashMapUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.CommandCallback; +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.exception.ConsumeQueueException; +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 static final String BORN_TIME = "bornTime"; + + private final BrokerController brokerController; + private final Random random = new Random(System.currentTimeMillis()); + private final String reviveTopic; + + 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, false); + this.queueLockManager = new QueueLockManager(); + this.popBufferMergeService = new PopBufferMergeService(this.brokerController, this); + this.ckMessageNumber = new AtomicLong(); + } + + protected String getReviveTopic() { + return reviveTopic; + } + + 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) throws ConsumeQueueException { + this.notifyLongPollingRequestIfNeed( + topic, group, queueId, null, 0L, null, null); + } + + public void notifyLongPollingRequestIfNeed(String topic, String group, int queueId, + Long tagsCode, long msgStoreTime, byte[] filterBitMap, + Map properties) throws ConsumeQueueException { + 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, -1, group, tagsCode, msgStoreTime, filterBitMap, properties); + if (!notifySuccess) { + // notify pop queue + notifySuccess = popLongPollingService.notifyMessageArriving( + topic, queueId, group, tagsCode, msgStoreTime, filterBitMap, properties); + } + 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, long offset, + Long tagsCode, long msgStoreTime, byte[] filterBitMap, Map properties) { + popLongPollingService.notifyMessageArrivingWithRetryTopic( + topic, queueId, offset, tagsCode, msgStoreTime, filterBitMap, properties); + } + + public void notifyMessageArriving(final String topic, final int queueId, final String cid) { + popLongPollingService.notifyMessageArriving( + topic, queueId, cid, false, null, 0L, null, null); + } + + @Override + public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingCommand request) + throws RemotingCommandException { + + final long beginTimeMills = this.brokerController.getMessageStore().now(); + + // fill bron time to properties if not exist, why we need this? + 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); + response.setOpaque(request.getOpaque()); + + final PopMessageRequestHeader requestHeader = + request.decodeCommandCustomHeader(PopMessageRequestHeader.class, true); + final PopMessageResponseHeader responseHeader = (PopMessageResponseHeader) response.readCustomHeader(); + + // Pop mode only supports consumption in cluster load balancing mode + brokerController.getConsumerManager().compensateBasicConsumerInfo( + requestHeader.getConsumerGroup(), ConsumeType.CONSUME_POP, MessageModel.CLUSTERING); + + 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.INVALID_PARAMETER); + 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.INVALID_PARAMETER); + 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(); + SubscriptionData subscriptionData = null; + ExpressionMessageFilter messageFilter = null; + if (requestHeader.getExp() != null && !requestHeader.getExp().isEmpty()) { + try { + // origin topic + subscriptionData = FilterAPI.build( + requestHeader.getTopic(), requestHeader.getExp(), requestHeader.getExpType()); + brokerController.getConsumerManager().compensateSubscribeData( + requestHeader.getConsumerGroup(), requestHeader.getTopic(), subscriptionData); + + // retry topic + 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 { + // origin topic + subscriptionData = FilterAPI.build(requestHeader.getTopic(), "*", ExpressionType.TAG); + brokerController.getConsumerManager().compensateSubscribeData( + requestHeader.getConsumerGroup(), requestHeader.getTopic(), subscriptionData); + + // retry topic + 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()); + } + } + + GetMessageResult getMessageResult = new GetMessageResult(requestHeader.getMaxMsgNums()); + ExpressionMessageFilter finalMessageFilter = messageFilter; + SubscriptionData finalSubscriptionData = subscriptionData; + + if (brokerConfig.isPopConsumerKVServiceEnable()) { + + CompletableFuture popAsyncFuture = brokerController.getPopConsumerService().popAsync( + RemotingHelper.parseChannelRemoteAddr(channel), beginTimeMills, requestHeader.getInvisibleTime(), + requestHeader.getConsumerGroup(), requestHeader.getTopic(), requestHeader.getQueueId(), + requestHeader.getMaxMsgNums(), requestHeader.isOrder(), + requestHeader.getAttemptId(), requestHeader.getInitMode(), messageFilter); + + popAsyncFuture.thenApply(result -> { + if (result.isFound()) { + response.setCode(ResponseCode.SUCCESS); + getMessageResult.setStatus(GetMessageStatus.FOUND); + // recursive processing + if (result.getRestCount() > 0) { + popLongPollingService.notifyMessageArriving( + requestHeader.getTopic(), requestHeader.getQueueId(), requestHeader.getConsumerGroup(), + null, 0L, null, null); + } + } else { + POP_LOGGER.debug("Processor not found, polling request, popTime={}, restCount={}", + result.getPopTime(), result.getRestCount()); + + PollingResult pollingResult = popLongPollingService.polling( + ctx, request, new PollingHeader(requestHeader), finalSubscriptionData, finalMessageFilter); + + if (PollingResult.POLLING_SUC == pollingResult) { + // recursive processing + if (result.getRestCount() > 0) { + popLongPollingService.notifyMessageArriving( + requestHeader.getTopic(), requestHeader.getQueueId(), requestHeader.getConsumerGroup(), + null, 0L, null, null); + } + return null; + } else if (PollingResult.POLLING_FULL == pollingResult) { + response.setCode(ResponseCode.POLLING_FULL); + } else { + response.setCode(ResponseCode.POLLING_TIMEOUT); + } + getMessageResult.setStatus(GetMessageStatus.NO_MESSAGE_IN_QUEUE); + } + + responseHeader.setPopTime(result.getPopTime()); + responseHeader.setInvisibleTime(result.getInvisibleTime()); + responseHeader.setReviveQid( + requestHeader.isOrder() ? KeyBuilder.POP_ORDER_REVIVE_QUEUE : 0); + responseHeader.setRestNum(result.getRestCount()); + responseHeader.setStartOffsetInfo(result.getStartOffsetInfo()); + responseHeader.setMsgOffsetInfo(result.getMsgOffsetInfo()); + if (requestHeader.isOrder() && !result.getOrderCountInfo().isEmpty()) { + responseHeader.setOrderCountInfo(result.getOrderCountInfo()); + } + + response.setRemark(getMessageResult.getStatus().name()); + if (response.getCode() != ResponseCode.SUCCESS) { + return response; + } + + // add message + result.getGetMessageResultList().forEach(temp -> { + for (int i = 0; i < temp.getMessageMapedList().size(); i++) { + getMessageResult.addMessage(temp.getMessageMapedList().get(i)); + } + }); + + 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); + 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(response.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; + } + return response; + }).thenAccept(result -> NettyRemotingAbstract.writeResponse(channel, request, result)); + return null; + } + + 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()); + } + + StringBuilder startOffsetInfo = new StringBuilder(64); + StringBuilder msgOffsetInfo = new StringBuilder(64); + StringBuilder orderCountInfo = requestHeader.isOrder() ? new StringBuilder(64) : null; + + // 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 < brokerConfig.getPopFromRetryProbability(); + 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, orderCountInfo)); + } + // 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 -> { + try { + if (request.getCallbackList() != null) { + request.getCallbackList().forEach(CommandCallback::accept); + request.getCallbackList().clear(); + } + } catch (Throwable t) { + POP_LOGGER.error("PopProcessor execute callback error", t); + } + + 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.getQueueId(), requestHeader.getConsumerGroup(), + null, 0L, null, null); + } + } else { + PollingResult pollingResult = popLongPollingService.polling( + ctx, request, new PollingHeader(requestHeader), finalSubscriptionData, finalMessageFilter); + if (PollingResult.POLLING_SUC == pollingResult) { + if (restNum > 0) { + popLongPollingService.notifyMessageArriving( + requestHeader.getTopic(), requestHeader.getQueueId(), requestHeader.getConsumerGroup(), + null, 0L, null, null); + } + 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() && orderCountInfo != null) { + responseHeader.setOrderCountInfo(orderCountInfo.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; + try { + offset = getPopOffset(topic, requestHeader.getConsumerGroup(), queueId, requestHeader.getInitMode(), + false, lockKey, false); + } catch (ConsumeQueueException e) { + CompletableFuture failure = new CompletableFuture<>(); + failure.completeExceptionally(e); + return failure; + } + + CompletableFuture future = new CompletableFuture<>(); + if (!queueLockManager.tryLock(lockKey)) { + try { + if (!requestHeader.isOrder()) { + restNum = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId) - offset + restNum; + } + future.complete(restNum); + } catch (ConsumeQueueException e) { + future.completeExceptionally(e); + } + return future; + } + + future.whenComplete((result, throwable) -> queueLockManager.unLock(lockKey)); + if (isPopShouldStop(topic, requestHeader.getConsumerGroup(), queueId)) { + POP_LOGGER.warn("Too much msgs unacked, then stop popping. topic={}, group={}, queueId={}", + topic, requestHeader.getConsumerGroup(), queueId); + try { + restNum = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId) - offset + restNum; + future.complete(restNum); + } catch (ConsumeQueueException e) { + future.completeExceptionally(e); + } + return future; + } + + try { + offset = getPopOffset(topic, requestHeader.getConsumerGroup(), queueId, requestHeader.getInitMode(), + true, lockKey, true); + + // Current requests would calculate the total number of messages + // waiting to be filtered for new message arrival notifications in + // the long-polling service, need disregarding the backlog in order + // consumption scenario. If rest message num including the blocked + // queue accumulation would lead to frequent unnecessary wake-ups + // of long-polling requests, resulting unnecessary CPU usage. + // When client ack message, long-polling request would be notifications + // by AckMessageProcessor.ackOrderly() and message will not be delayed. + if (isOrder) { + if (brokerController.getConsumerOrderInfoManager().checkBlock( + attemptId, topic, requestHeader.getConsumerGroup(), queueId, requestHeader.getInvisibleTime())) { + // should not add accumulation(max offset - consumer offset) here + future.complete(restNum); + return future; + } + 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) { + try { + atomicRestNum.set(brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId) - atomicOffset.get() + atomicRestNum.get()); + } catch (ConsumeQueueException e) { + POP_LOGGER.error("Failed to get max offset in queue", e); + } + 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) { + if (isOrder) { + this.brokerController.getConsumerOffsetManager().commitOffset(channel.remoteAddress().toString(), requestHeader.getConsumerGroup(), topic, + queueId, result.getNextBeginOffset()); + } else { + popBufferMergeService.addCkMock(requestHeader.getConsumerGroup(), topic, queueId, finalOffset, + requestHeader.getInvisibleTime(), popTime, reviveQid, result.getNextBeginOffset(), brokerController.getBrokerConfig().getBrokerName()); + } + } + + 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) throws ConsumeQueueException { + + 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); + } + } + + public long getInitOffset(String topic, String group, int queueId, int initMode, boolean init) + throws ConsumeQueueException { + long offset; + if (ConsumeInitMode.MIN == initMode || topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + offset = 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) { // whichever initMode + this.brokerController.getConsumerOffsetManager().commitOffset( + "getPopOffset", group, topic, queueId, offset); + } + return offset; + } + + public MessageExtBrokerInner buildCkMsg(final PopCheckPoint ck, final int reviveQid) { + MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + + msgInner.setTopic(reviveTopic); + msgInner.setBody(JSON.toJSONString(ck).getBytes(StandardCharsets.UTF_8)); + 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)); + } + + this.brokerController.getBrokerStatsManager().incBrokerCkNums(1); + this.brokerController.getBrokerStatsManager().incGroupCkNums(requestHeader.getConsumerGroup(), requestHeader.getTopic(), 1); + + 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() { + // init lock status, false means not locked + this.lock = new AtomicBoolean(false); + this.lockTime = System.currentTimeMillis(); + } + + public boolean tryLock() { + boolean ret = lock.compareAndSet(false, true); + if (ret) { + this.lockTime = System.currentTimeMillis(); + return true; + } else { + return false; + } + } + + public void unLock() { + lock.set(false); + } + + 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 = ConcurrentHashMapUtils.computeIfAbsent(expiredLocalCache, key, k -> new TimedLock()); + 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 0000000..2be41a6 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java @@ -0,0 +1,703 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.nio.charset.StandardCharsets; +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.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Triple; +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.UtilAll; +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.PutMessageResult; +import org.apache.rocketmq.store.exception.ConsumeQueueException; +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 final int[] ckRewriteIntervalsInSeconds = new int[] { 10, 20, 30, 60, 120, 180, 240, 300, 360, 420, 480, 540, 600, 1200, 1800, 3600, 7200 }; + + 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())); + addRetryTopicIfNotExist(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); + } + } + + public void addRetryTopicIfNotExist(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(); + } + + // Triple + public CompletableFuture> getBizMessage(PopCheckPoint popCheckPoint, long offset) { + return this.brokerController.getEscapeBridge().getMessageAsync(popCheckPoint.getTopic(), offset, popCheckPoint.getQueueId(), popCheckPoint.getBrokerName(), 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 { + try { + 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); + } + } catch (ConsumeQueueException e) { + POP_LOGGER.error("Failed to get max offset in queue", e); + } + 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.debug("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.getBrokerName(), 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(), StandardCharsets.UTF_8); + 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 brokerName = StringUtils.isNotBlank(ackMsg.getBrokerName()) ? + ackMsg.getBrokerName() : brokerController.getBrokerConfig().getBrokerName(); + String mergeKey = ackMsg.getTopic() + ackMsg.getConsumerGroup() + ackMsg.getQueueId() + ackMsg.getStartOffset() + ackMsg.getPopTime() + brokerName; + 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(), StandardCharsets.UTF_8); + 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 brokerName = StringUtils.isNotBlank(bAckMsg.getBrokerName()) ? + bAckMsg.getBrokerName() : brokerController.getBrokerConfig().getBrokerName(); + String mergeKey = bAckMsg.getTopic() + bAckMsg.getConsumerGroup() + bAckMsg.getQueueId() + bAckMsg.getStartOffset() + bAckMsg.getPopTime() + brokerName; + 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); + POP_LOGGER.warn("stay too long, remove from reviveRequestMap, {}, {}, {}, {}", popCheckPoint.getTopic(), + popCheckPoint.getBrokerName(), popCheckPoint.getQueueId(), popCheckPoint.getStartOffset()); + } + } + + 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, msgOffset) + .thenApply(rst -> { + MessageExt message = rst.getLeft(); + if (message == null) { + POP_LOGGER.info("reviveQueueId={}, can not get biz msg, topic:{}, qid:{}, offset:{}, brokerName:{}, info:{}, retry:{}, then continue", + queueId, popCheckPoint.getTopic(), popCheckPoint.getQueueId(), msgOffset, popCheckPoint.getBrokerName(), UtilAll.frontStringAtLeast(rst.getMiddle(), 60), rst.getRight()); + return new Pair<>(msgOffset, !rst.getRight()); // Pair.object2 means OK or not, Triple.right value means needRetry + } + 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) { + int rePutTimes = oldCK.parseRePutTimes(); + if (rePutTimes >= ckRewriteIntervalsInSeconds.length && brokerController.getBrokerConfig().isSkipWhenCKRePutReachMaxTimes()) { + POP_LOGGER.warn("rePut CK reach max times, drop it. {}, {}, {}, {}-{}, {}, {}, {}", oldCK.getTopic(), oldCK.getCId(), + oldCK.getBrokerName(), oldCK.getQueueId(), pair.getObject1(), oldCK.getPopTime(), oldCK.getInvisibleTime(), rePutTimes); + return; + } + + 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); + newCk.setRePutTimes(String.valueOf(rePutTimes + 1)); // always increment even if removed from reviveRequestMap + if (oldCK.getReviveTime() <= System.currentTimeMillis()) { + // never expect an ACK matched in the future, we just use it to rewrite CK and try to revive retry message next time + int intervalIndex = rePutTimes >= ckRewriteIntervalsInSeconds.length ? ckRewriteIntervalsInSeconds.length - 1 : rePutTimes; + newCk.setInvisibleTime(oldCK.getInvisibleTime() + ckRewriteIntervalsInSeconds[intervalIndex] * 1000); + } + MessageExtBrokerInner ckMsg = brokerController.getPopMessageProcessor().buildCkMsg(newCk, queueId); + brokerController.getMessageStore().putMessage(ckMsg); + } + + public long getReviveBehindMillis() throws ConsumeQueueException { + 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() throws ConsumeQueueException { + if (currentReviveMessageTimestamp <= 0) { + return 0; + } + // the next pull offset is reviveOffset + 1 + long diff = brokerController.getMessageStore().getMaxOffsetInQueue(reviveTopic, queueId) - reviveOffset - 1; + 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 new file mode 100644 index 0000000..5d947fd --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PullMessageProcessor.java @@ -0,0 +1,910 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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; +import org.apache.rocketmq.broker.filter.ExpressionMessageFilter; +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.plugin.PullMessageResultHandler; +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.filter.ExpressionType; +import org.apache.rocketmq.common.help.FAQUrl; +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.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.GetMessageStatus; +import org.apache.rocketmq.store.MessageFilter; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.exception.ConsumeQueueException; +import org.apache.rocketmq.store.stats.BrokerStatsManager; + +import static org.apache.rocketmq.remoting.protocol.RemotingCommand.buildErrorResponse; + +public class PullMessageProcessor implements NettyRequestProcessor { + 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, 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, + 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 = + (PullMessageRequestHeader) request.decodeCommandCustomHeader(PullMessageRequestHeader.class); + + response.setOpaque(request.getOpaque()); + + LOGGER.debug("receive PullMessage request command, {}", request); + + if (!PermName.isReadable(this.brokerController.getBrokerConfig().getBrokerPermission())) { + response.setCode(ResponseCode.NO_PERMISSION); + 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; + } + + 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); + responseHeader.setForbiddenType(ForbiddenType.GROUP_FORBIDDEN); + response.setRemark("subscription group no permission, " + requestHeader.getConsumerGroup()); + return response; + } + + TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic()); + if (null == topicConfig) { + 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); + 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()); + LOGGER.warn(errorInfo); + response.setCode(ResponseCode.INVALID_PARAMETER); + 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(), + requestHeader.getExpressionType(), requestHeader.getSubVersion() + ); + assert consumerFilterData != null; + } + } catch (Exception e) { + 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"); + return response; + } + } else { + ConsumerGroupInfo consumerGroupInfo = + this.brokerController.getConsumerManager().getConsumerGroupInfo(requestHeader.getConsumerGroup()); + if (null == consumerGroupInfo) { + 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; + } + + 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) { + 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()) { + 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"); + return response; + } + if (!ExpressionType.isTagType(subscriptionData.getExpressionType())) { + consumerFilterData = this.brokerController.getConsumerFilterManager().get(requestHeader.getTopic(), + requestHeader.getConsumerGroup()); + if (consumerFilterData == null) { + response.setCode(ResponseCode.FILTER_DATA_NOT_EXIST); + response.setRemark("The broker's consumer filter data is not exist!Your expression may be wrong!"); + return response; + } + if (consumerFilterData.getClientVersion() < requestHeader.getSubVersion()) { + 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"); + return response; + } + } + } + + if (!ExpressionType.isTagType(subscriptionData.getExpressionType()) + && !this.brokerController.getBrokerConfig().isEnablePropertyFilter()) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("The broker does not support consumer to filter message by " + subscriptionData.getExpressionType()); + return response; + } + + MessageFilter messageFilter; + if (this.brokerController.getBrokerConfig().isFilterSupportRetry()) { + messageFilter = new ExpressionForRetryMessageFilter(subscriptionData, consumerFilterData, + this.brokerController.getConsumerFilterManager()); + } else { + messageFilter = new ExpressionMessageFilter(subscriptionData, consumerFilterData, + this.brokerController.getConsumerFilterManager()); + } + + if (brokerController.getBrokerConfig().isRejectPullConsumerEnable()) { + ConsumerGroupInfo consumerGroupInfo = + this.brokerController.getConsumerManager().getConsumerGroupInfo(requestHeader.getConsumerGroup()); + if (null == consumerGroupInfo || ConsumeType.CONSUME_ACTIVELY == consumerGroupInfo.getConsumeType()) { + if ((null == consumerGroupInfo || null == consumerGroupInfo.findChannel(channel)) + && !MixAll.isSysConsumerGroupPullMessage(requestHeader.getConsumerGroup())) { + response.setCode(ResponseCode.SUBSCRIPTION_NOT_EXIST); + response.setRemark("the consumer's group info not exist, or the pull consumer is rejected by server." + FAQUrl.suggestTodo(FAQUrl.SUBSCRIPTION_GROUP_NOT_EXIST)); + return response; + } + } + } + + 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); + } + } + } + } + + 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)); + try { + getMessageResult.setMaxOffset(messageStore.getMaxOffsetInQueue(topic, queueId)); + } catch (ConsumeQueueException e) { + throw new RemotingCommandException("Failed tp get max offset in queue", e); + } + 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 + ); + }) + .thenAccept(result -> NettyRemotingAbstract.writeResponse(channel, request, result)); + } + } + + if (getMessageResult != null) { + + return this.pullMessageResultHandler.handle( + getMessageResult, + request, + requestHeader, + channel, + subscriptionData, + subscriptionGroupConfig, + brokerAllowSuspend, + messageFilter, + response, + mappingContext, + beginTimeMills + ); + } + return null; + } + + public boolean hasConsumeMessageHook() { + return consumeMessageHookList != null && !this.consumeMessageHookList.isEmpty(); + } + + /** + * 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; + } + + 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); + } + + 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); + } + } + } + + } + + 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())); + context.setFilterMessageCount(getMessageResult.getFilterMessageCount()); + + switch (responseCode) { + case ResponseCode.SUCCESS: + int commercialBaseCount = brokerController.getBrokerConfig().getCommercialBaseCount(); + int incValue = getMessageResult.getMsgCount4Commercial() * commercialBaseCount; + + context.setCommercialRcvStats(BrokerStatsManager.StatsType.RCV_SUCCESS); + context.setCommercialRcvTimes(incValue); + context.setCommercialRcvSize(getMessageResult.getBufferTotalSize()); + context.setCommercialOwner(owner); + + context.setRcvStat(BrokerStatsManager.StatsType.RCV_SUCCESS); + context.setRcvMsgNum(getMessageResult.getMessageCount()); + context.setRcvMsgSize(getMessageResult.getBufferTotalSize()); + context.setCommercialRcvMsgNum(getMessageResult.getMsgCount4Commercial()); + + break; + case ResponseCode.PULL_NOT_FOUND: + if (!brokerAllowSuspend) { + + 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; + case ResponseCode.PULL_RETRY_IMMEDIATELY: + case ResponseCode.PULL_OFFSET_MOVED: + 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) { + } + } + } + } + + 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; + if (storeOffsetEnable) { + this.brokerController.getConsumerOffsetManager().commitOffset(clientAddress, requestHeader.getConsumerGroup(), + requestHeader.getTopic(), requestHeader.getQueueId(), requestHeader.getCommitOffset()); + } + } + + 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("executeRequestWhenWakeup run", e1); + } + }; + this.brokerController.getPullMessageExecutor().submit(new RequestTask(run, channel, request)); + } + + public void registerConsumeMessageHook(List consumeMessageHookList) { + this.consumeMessageHookList = consumeMessageHookList; + } + + public void setPullMessageResultHandler(PullMessageResultHandler pullMessageResultHandler) { + this.pullMessageResultHandler = pullMessageResultHandler; + } + + 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; + } + + boolean proxyPullBroadcast = Objects.equals( + RequestSource.PROXY_FOR_BROADCAST.getValue(), requestHeader.getRequestSource()); + ConsumerGroupInfo consumerGroupInfo = this.brokerController.getConsumerManager().getConsumerGroupInfo(group); + + 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); + } + } + + /** + * When pull request is not broadcast or not return -1 + */ + protected long queryBroadcastPullInitOffset(String topic, String group, int queueId, + PullMessageRequestHeader requestHeader, Channel channel) throws RemotingCommandException { + + 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(); + } + + try { + return this.brokerController.getBroadcastOffsetManager() + .queryInitOffset(topic, group, queueId, clientId, requestHeader.getQueueOffset(), proxyPullBroadcast); + } catch (ConsumeQueueException e) { + throw new RemotingCommandException("Failed to query initial offset", e); + } + } + 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 0000000..d29e3d0 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/QueryAssignmentProcessor.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.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.TopicConfig; +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; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; + +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; + if (MixAll.isLmq(topic)) { + mqSet = new HashSet<>(); + mqSet.add(new MessageQueue( + topic, brokerController.getBrokerConfig().getBrokerName(), (int)MixAll.LMQ_QUEUE_ID)); + } else { + 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; + } + TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(topic); + if (null == topicConfig) { + response.setCode(ResponseCode.TOPIC_NOT_EXIST); + response.setRemark("topic[" + topic + "] not exist"); + return response; + } + + final String consumerGroup = requestBody.getConsumerGroup(); + SubscriptionGroupConfig groupConfig = this.brokerController.getSubscriptionGroupManager().findSubscriptionGroupConfig(consumerGroup); + if (null == groupConfig) { + response.setCode(ResponseCode.SUBSCRIPTION_GROUP_NOT_EXIST); + response.setRemark("subscription group does not exist"); + return response; + } + + 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 new file mode 100644 index 0000000..3838514 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/QueryMessageProcessor.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.broker.processor; + +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.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 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) { + this.brokerController = brokerController; + } + + @Override + public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) + throws RemotingCommandException { + switch (request.getCode()) { + case RequestCode.QUERY_MESSAGE: + return this.queryMessage(ctx, request); + case RequestCode.VIEW_MESSAGE_BY_ID: + return this.viewMessageById(ctx, request); + default: + break; + } + + return null; + } + + @Override + public boolean rejectRequest() { + return false; + } + + public RemotingCommand queryMessage(ChannelHandlerContext ctx, RemotingCommand request) + throws RemotingCommandException { + final RemotingCommand response = + RemotingCommand.createResponseCommand(QueryMessageResponseHeader.class); + final QueryMessageResponseHeader responseHeader = + (QueryMessageResponseHeader) response.readCustomHeader(); + final QueryMessageRequestHeader requestHeader = + (QueryMessageRequestHeader) request + .decodeCommandCustomHeader(QueryMessageRequestHeader.class); + + response.setOpaque(request.getOpaque()); + + String isUniqueKey = request.getExtFields().get(MixAll.UNIQUE_MSG_QUERY_FLAG); + if (isUniqueKey != null && isUniqueKey.equals("true")) { + requestHeader.setMaxNum(this.brokerController.getMessageStoreConfig().getDefaultQueryMaxNum()); + } + + final QueryMessageResult queryMessageResult = + this.brokerController.getMessageStore().queryMessage(requestHeader.getTopic(), + requestHeader.getKey(), requestHeader.getMaxNum(), requestHeader.getBeginTimestamp(), + requestHeader.getEndTimestamp()); + assert queryMessageResult != null; + + responseHeader.setIndexLastUpdatePhyoffset(queryMessageResult.getIndexLastUpdatePhyoffset()); + responseHeader.setIndexLastUpdateTimestamp(queryMessageResult.getIndexLastUpdateTimestamp()); + + if (queryMessageResult.getBufferTotalSize() > 0) { + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + + try { + FileRegion fileRegion = + new QueryMessageTransfer(response.encodeHeader(queryMessageResult + .getBufferTotalSize()), queryMessageResult); + 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()) { + LOGGER.error("transfer query message by page cache failed, ", future.cause()); + } + }); + } catch (Throwable e) { + LOGGER.error("", e); + queryMessageResult.release(); + } + + return null; + } + + response.setCode(ResponseCode.QUERY_NOT_FOUND); + response.setRemark("can not find message, maybe time range not correct"); + return response; + } + + public RemotingCommand viewMessageById(ChannelHandlerContext ctx, RemotingCommand request) + throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + final ViewMessageRequestHeader requestHeader = + (ViewMessageRequestHeader) request.decodeCommandCustomHeader(ViewMessageRequestHeader.class); + + response.setOpaque(request.getOpaque()); + + final SelectMappedBufferResult selectMappedBufferResult = + this.brokerController.getMessageStore().selectOneMessageByOffset(requestHeader.getOffset()); + if (selectMappedBufferResult != null) { + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + + try { + FileRegion fileRegion = + new OneMessageTransfer(response.encodeHeader(selectMappedBufferResult.getSize()), + selectMappedBufferResult); + 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()) { + LOGGER.error("Transfer one message from page cache failed, ", future.cause()); + } + }); + } catch (Throwable e) { + LOGGER.error("", e); + selectMappedBufferResult.release(); + } + + return null; + } else { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("can not find message by the offset, " + requestHeader.getOffset()); + } + + return response; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/RecallMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/RecallMessageProcessor.java new file mode 100644 index 0000000..372db0d --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/RecallMessageProcessor.java @@ -0,0 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.commons.codec.DecoderException; +import org.apache.commons.lang3.math.NumberUtils; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.constant.PermName; +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.producer.RecallMessageHandle; +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.RecallMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.RecallMessageResponseHeader; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.timer.TimerMessageStore; + +import java.nio.charset.StandardCharsets; + +public class RecallMessageProcessor implements NettyRequestProcessor { + private static final String RECALL_MESSAGE_TAG = "_RECALL_TAG_"; + private final BrokerController brokerController; + + public RecallMessageProcessor(final BrokerController brokerController) { + this.brokerController = brokerController; + } + + @Override + public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) throws + RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(RecallMessageResponseHeader.class); + response.addExtField(MessageConst.PROPERTY_MSG_REGION, this.brokerController.getBrokerConfig().getRegionId()); + final RecallMessageRequestHeader requestHeader = + request.decodeCommandCustomHeader(RecallMessageRequestHeader.class); + + if (!brokerController.getBrokerConfig().isRecallMessageEnable()) { + response.setCode(ResponseCode.NO_PERMISSION); + response.setRemark("recall failed, operation is forbidden"); + return response; + } + + if (BrokerRole.SLAVE == brokerController.getMessageStoreConfig().getBrokerRole()) { + response.setCode(ResponseCode.SLAVE_NOT_AVAILABLE); + response.setRemark("recall failed, broker service not available"); + return response; + } + + final long startTimestamp = this.brokerController.getBrokerConfig().getStartAcceptSendRequestTimeStamp(); + if (this.brokerController.getMessageStore().now() < startTimestamp) { + response.setCode(ResponseCode.SERVICE_NOT_AVAILABLE); + response.setRemark("recall failed, broker service not available"); + return response; + } + + if (!PermName.isWriteable(this.brokerController.getBrokerConfig().getBrokerPermission()) + && !this.brokerController.getBrokerConfig().isAllowRecallWhenBrokerNotWriteable()) { + response.setCode(ResponseCode.SERVICE_NOT_AVAILABLE); + response.setRemark("recall failed, broker service not available"); + return response; + } + + TopicConfig topicConfig = + this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic()); + if (null == topicConfig) { + response.setCode(ResponseCode.TOPIC_NOT_EXIST); + response.setRemark("recall failed, the topic[" + requestHeader.getTopic() + "] not exist"); + return response; + } + + RecallMessageHandle.HandleV1 handle; + try { + handle = (RecallMessageHandle.HandleV1) RecallMessageHandle.decodeHandle(requestHeader.getRecallHandle()); + } catch (DecoderException e) { + response.setCode(ResponseCode.ILLEGAL_OPERATION); + response.setRemark(e.getMessage()); + return response; + } + + if (!requestHeader.getTopic().equals(handle.getTopic())) { + response.setCode(ResponseCode.ILLEGAL_OPERATION); + response.setRemark("recall failed, topic not match"); + return response; + } + if (!brokerController.getBrokerConfig().getBrokerName().equals(handle.getBrokerName())) { + response.setCode(ResponseCode.ILLEGAL_OPERATION); + response.setRemark("recall failed, broker service not available"); + return response; + } + + long timestamp = NumberUtils.toLong(handle.getTimestampStr(), -1); + long timeLeft = timestamp - System.currentTimeMillis(); + if (timeLeft <= 0 + || timeLeft >= brokerController.getMessageStoreConfig().getTimerMaxDelaySec() * 1000L) { + response.setCode(ResponseCode.ILLEGAL_OPERATION); + response.setRemark("recall failed, timestamp invalid"); + return response; + } + + MessageExtBrokerInner msgInner = buildMessage(ctx, requestHeader, handle); + long beginTimeMillis = this.brokerController.getMessageStore().now(); + PutMessageResult putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner); + handlePutMessageResult(putMessageResult, request, response, msgInner, ctx, beginTimeMillis); + return response; + } + + public MessageExtBrokerInner buildMessage(ChannelHandlerContext ctx, RecallMessageRequestHeader requestHeader, + RecallMessageHandle.HandleV1 handle) { + MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + msgInner.setTopic(handle.getTopic()); + msgInner.setBody("0".getBytes(StandardCharsets.UTF_8)); + msgInner.setTags(RECALL_MESSAGE_TAG); + msgInner.setTagsCode(RECALL_MESSAGE_TAG.hashCode()); + msgInner.setQueueId(0); + MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_TIMER_DEL_UNIQKEY, + TimerMessageStore.buildDeleteKey(handle.getTopic(), handle.getMessageId())); + MessageAccessor.putProperty(msgInner, + MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, handle.getMessageId()); + MessageAccessor.putProperty(msgInner, + MessageConst.PROPERTY_TIMER_DELIVER_MS, String.valueOf(handle.getTimestampStr())); + MessageAccessor.putProperty(msgInner, + MessageConst.PROPERTY_BORN_TIMESTAMP, String.valueOf(System.currentTimeMillis())); + MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_TRACE_CONTEXT, ""); + MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_PRODUCER_GROUP, requestHeader.getProducerGroup()); + msgInner.setBornTimestamp(System.currentTimeMillis()); + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); + msgInner.setBornHost(ctx.channel().remoteAddress()); + msgInner.setStoreHost(this.brokerController.getStoreHost()); + return msgInner; + } + + public void handlePutMessageResult(PutMessageResult putMessageResult, RemotingCommand request, + RemotingCommand response, MessageExt message, ChannelHandlerContext ctx, long beginTimeMillis) { + if (null == putMessageResult) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("recall failed, execute error"); + return; + } + RecallMessageResponseHeader responseHeader = (RecallMessageResponseHeader) response.readCustomHeader(); + switch (putMessageResult.getPutMessageStatus()) { + case PUT_OK: + this.brokerController.getBrokerStatsManager().incTopicPutNums( + message.getTopic(), putMessageResult.getAppendMessageResult().getMsgNum(), 1); // system timer topic + this.brokerController.getBrokerStatsManager().incTopicPutSize( + message.getTopic(), putMessageResult.getAppendMessageResult().getWroteBytes()); + this.brokerController.getBrokerStatsManager().incBrokerPutNums( + message.getTopic(), putMessageResult.getAppendMessageResult().getMsgNum()); + this.brokerController.getBrokerStatsManager().incTopicPutLatency( + message.getTopic(), 0, (int) (this.brokerController.getMessageStore().now() - beginTimeMillis)); + case FLUSH_DISK_TIMEOUT: + case FLUSH_SLAVE_TIMEOUT: + case SLAVE_NOT_AVAILABLE: + response.setCode(ResponseCode.SUCCESS); + responseHeader.setMsgId(MessageClientIDSetter.getUniqID(message)); + break; + default: + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("recall failed, execute error"); + break; + } + } + + @Override + public boolean rejectRequest() { + return false; + } +} 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 0000000..a70b48d --- /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 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); + 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 0000000..a6019dc --- /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 new file mode 100644 index 0000000..669cd5e --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/SendMessageProcessor.java @@ -0,0 +1,717 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.opentelemetry.api.common.Attributes; +import org.apache.commons.lang3.StringUtils; +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.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.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.producer.RecallMessageHandle; +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.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.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; + +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; + +public class SendMessageProcessor extends AbstractSendMessageProcessor implements NettyRequestProcessor { + + public SendMessageProcessor(final BrokerController brokerController) { + super(brokerController); + } + + @Override + public RemotingCommand processRequest(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + SendMessageContext sendMessageContext; + switch (request.getCode()) { + case RequestCode.CONSUMER_SEND_MSG_BACK: + return this.consumerSendMsgBack(ctx, request); + default: + SendMessageRequestHeader requestHeader = parseRequestHeader(request); + if (requestHeader == null) { + return null; + } + 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, sendMessageContext, requestHeader, mappingContext, + (ctx1, response1) -> executeSendMessageHookAfter(response1, ctx1)); + } else { + response = this.sendMessage(ctx, request, sendMessageContext, requestHeader, mappingContext, + (ctx12, response12) -> executeSendMessageHookAfter(response12, ctx12)); + } + + return response; + } + } + + @Override + public boolean rejectRequest() { + if (!this.brokerController.getBrokerConfig().isEnableSlaveActingMaster() && this.brokerController.getMessageStoreConfig().getBrokerRole() == BrokerRole.SLAVE) { + return true; + } + + if (this.brokerController.getMessageStore().isOSPageCacheBusy() || this.brokerController.getMessageStore().isTransientStorePoolDeficient()) { + return true; + } + + return false; + } + + private void clearReservedProperties(SendMessageRequestHeader requestHeader) { + String properties = requestHeader.getProperties(); + properties = MessageUtils.deleteProperty(properties, MessageConst.PROPERTY_POP_CK); + requestHeader.setProperties(properties); + } + + /** + * 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; + } + TopicQueueMappingDetail mappingDetail = mappingContext.getMappingDetail(); + + 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())); + } + //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()); + } + return null; + } + + private boolean handleRetryAndDLQ(SendMessageRequestHeader requestHeader, RemotingCommand response, + RemotingCommand request, + MessageExt msg, TopicConfig topicConfig, Map properties) { + String newTopic = requestHeader.getTopic(); + if (null != newTopic && newTopic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + String groupName = KeyBuilder.parseGroup(newTopic); + SubscriptionGroupConfig subscriptionGroupConfig = + this.brokerController.getSubscriptionGroupManager().findSubscriptionGroupConfig(groupName); + if (null == subscriptionGroupConfig) { + response.setCode(ResponseCode.SUBSCRIPTION_GROUP_NOT_EXIST); + response.setRemark( + "subscription group not exist, " + groupName + " " + FAQUrl.suggestTodo(FAQUrl.SUBSCRIPTION_GROUP_NOT_EXIST)); + return false; + } + + int maxReconsumeTimes = subscriptionGroupConfig.getRetryMaxTimes(); + if (request.getVersion() >= MQVersion.Version.V3_4_9.ordinal() && requestHeader.getMaxReconsumeTimes() != null) { + maxReconsumeTimes = requestHeader.getMaxReconsumeTimes(); + } + int reconsumeTimes = requestHeader.getReconsumeTimes() == null ? 0 : requestHeader.getReconsumeTimes(); + + 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 = randomQueueId(DLQ_NUMS_PER_GROUP); + topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(newTopic, + DLQ_NUMS_PER_GROUP, + 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"); + return false; + } + } + } + int sysFlag = requestHeader.getSysFlag(); + if (TopicFilterType.MULTI_TAG == topicConfig.getTopicFilterType()) { + sysFlag |= MessageSysFlag.MULTI_TAGS_FLAG; + } + msg.setSysFlag(sysFlag); + return true; + } + + public RemotingCommand sendMessage(final ChannelHandlerContext ctx, + final RemotingCommand request, + final SendMessageContext sendMessageContext, + final SendMessageRequestHeader requestHeader, + final TopicQueueMappingContext mappingContext, + final SendMessageCallback sendMessageCallback) throws RemotingCommandException { + + 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 = randomQueueId(topicConfig.getWriteQueueNums()); + } + + MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + msgInner.setTopic(requestHeader.getTopic()); + msgInner.setQueueId(queueIdInt); + + Map oriProps = MessageDecoder.string2messageProperties(requestHeader.getProperties()); + if (!handleRetryAndDLQ(requestHeader, response, request, msgInner, topicConfig, oriProps)) { + return response; + } + + msgInner.setBody(body); + msgInner.setFlag(requestHeader.getFlag()); + + 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())); + + // 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"); + return response; + } + sendTransactionPrepareMessage = true; + } else { + sendTransactionPrepareMessage = false; + } + + 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); + } + + // 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, long beginTimeMillis, + TopicQueueMappingContext mappingContext, TopicMessageType messageType) { + if (putMessageResult == null) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("store putMessage return null"); + return response; + } + boolean sendOK = false; + + switch (putMessageResult.getPutMessageStatus()) { + // Success + case PUT_OK: + sendOK = true; + response.setCode(ResponseCode.SUCCESS); + break; + case FLUSH_DISK_TIMEOUT: + response.setCode(ResponseCode.FLUSH_DISK_TIMEOUT); + sendOK = true; + break; + case FLUSH_SLAVE_TIMEOUT: + response.setCode(ResponseCode.FLUSH_SLAVE_TIMEOUT); + sendOK = true; + break; + case SLAVE_NOT_AVAILABLE: + response.setCode(ResponseCode.SLAVE_NOT_AVAILABLE); + sendOK = true; + break; + + // 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(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. 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_PAGE_CACHE_BUSY: + response.setCode(ResponseCode.SYSTEM_BUSY); + 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"); + break; + default: + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("UNKNOWN_ERROR DEFAULT"); + break; + } + + 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(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)); + attachRecallHandle(request, msg, responseHeader); + + RemotingCommand rewriteResult = rewriteResponseForStaticTopic(responseHeader, mappingContext); + if (rewriteResult != null) { + return rewriteResult; + } + + doResponse(ctx, request, response); + + 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 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 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(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; + } + + private RemotingCommand sendBatchMessage(final ChannelHandlerContext ctx, + final RemotingCommand request, + final SendMessageContext sendMessageContext, + final SendMessageRequestHeader requestHeader, + TopicQueueMappingContext mappingContext, + final SendMessageCallback sendMessageCallback) { + final RemotingCommand response = preSend(ctx, request, requestHeader); + final SendMessageResponseHeader responseHeader = (SendMessageResponseHeader) response.readCustomHeader(); + + if (response.getCode() != -1) { + return response; + } + + int queueIdInt = requestHeader.getQueueId(); + TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic()); + + if (queueIdInt < 0) { + queueIdInt = randomQueueId(topicConfig.getWriteQueueNums()); + } + + if (requestHeader.getTopic().length() > Byte.MAX_VALUE) { + response.setCode(ResponseCode.MESSAGE_ILLEGAL); + response.setRemark("message topic length too long " + requestHeader.getTopic().length()); + return response; + } + + if (requestHeader.getTopic() != null && requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + response.setCode(ResponseCode.MESSAGE_ILLEGAL); + response.setRemark("batch request does not support retry group " + requestHeader.getTopic()); + return response; + } + MessageExtBatch messageExtBatch = new MessageExtBatch(); + messageExtBatch.setTopic(requestHeader.getTopic()); + messageExtBatch.setQueueId(queueIdInt); + + int sysFlag = requestHeader.getSysFlag(); + if (TopicFilterType.MULTI_TAG == topicConfig.getTopicFilterType()) { + sysFlag |= MessageSysFlag.MULTI_TAGS_FLAG; + } + messageExtBatch.setSysFlag(sysFlag); + + messageExtBatch.setFlag(requestHeader.getFlag()); + MessageAccessor.setProperties(messageExtBatch, MessageDecoder.string2messageProperties(requestHeader.getProperties())); + messageExtBatch.setBody(request.getBody()); + messageExtBatch.setBornTimestamp(requestHeader.getBornTimestamp()); + 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); + + boolean isInnerBatch = false; + + 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); + + int innerNum = MessageDecoder.countInnerMsgNum(ByteBuffer.wrap(messageExtBatch.getBody())); + + 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 void attachRecallHandle(RemotingCommand request, MessageExt msg, SendMessageResponseHeader responseHeader) { + if (RequestCode.SEND_BATCH_MESSAGE == request.getCode() + || RequestCode.CONSUMER_SEND_MSG_BACK == request.getCode()) { + return; + } + String timestampStr = msg.getProperty(MessageConst.PROPERTY_TIMER_OUT_MS); + String realTopic = msg.getProperty(MessageConst.PROPERTY_REAL_TOPIC); + if (timestampStr != null && realTopic != null && !realTopic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + timestampStr = String.valueOf(Long.parseLong(timestampStr) + 1); // consider of floor + String recallHandle = RecallMessageHandle.HandleV1.buildHandle(realTopic, + brokerController.getBrokerConfig().getBrokerName(), timestampStr, MessageClientIDSetter.getUniqID(msg)); + responseHeader.setRecallHandle(recallHandle); + } + } + + private String diskUtil() { + 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()); + double logisRatio = UtilAll.getDiskPartitionSpaceUsedPercent(storePathLogis); + + String storePathIndex = + StorePathConfigHelper.getStorePathIndex(this.brokerController.getMessageStoreConfig().getStorePathRootDir()); + double indexRatio = UtilAll.getDiskPartitionSpaceUsedPercent(storePathIndex); + + return String.format("CL: %5.2f CQ: %5.2f INDEX: %5.2f", physicRatio, logisRatio, indexRatio); + } + + 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/broker/src/main/java/org/apache/rocketmq/broker/schedule/DelayOffsetSerializeWrapper.java b/broker/src/main/java/org/apache/rocketmq/broker/schedule/DelayOffsetSerializeWrapper.java new file mode 100644 index 0000000..2485c9a --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/schedule/DelayOffsetSerializeWrapper.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.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); + + private DataVersion dataVersion; + + public ConcurrentMap getOffsetTable() { + return offsetTable; + } + + 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 0000000..a5b02c9 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/schedule/ScheduleMessageService.java @@ -0,0 +1,852 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.exception.ConsumeQueueException; +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) throws ConsumeQueueException { + 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 (Throwable 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(currOffset, 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 new file mode 100644 index 0000000..ee716a5 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/slave/SlaveSynchronize.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.broker.slave; + +import java.io.IOException; +import java.util.Iterator; +import java.util.Map; +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.loadbalance.MessageRequestModeManager; +import org.apache.rocketmq.broker.subscription.SubscriptionGroupManager; +import org.apache.rocketmq.broker.topic.TopicConfigManager; +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.body.ConsumerOffsetSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.body.MessageRequestModeSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.body.SetMessageRequestModeRequestBody; +import org.apache.rocketmq.remoting.protocol.body.SubscriptionGroupWrapper; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigAndMappingSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.store.config.StorePathConfigHelper; +import org.apache.rocketmq.store.timer.TimerCheckpoint; +import org.apache.rocketmq.store.timer.TimerMetrics; + +public class SlaveSynchronize { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + private final BrokerController brokerController; + private volatile String masterAddr = null; + + public SlaveSynchronize(BrokerController brokerController) { + this.brokerController = brokerController; + } + + public String getMasterAddr() { + return masterAddr; + } + + public void setMasterAddr(String masterAddr) { + if (!StringUtils.equals(this.masterAddr, masterAddr)) { + LOGGER.info("Update master address from {} to {}", this.masterAddr, masterAddr); + this.masterAddr = masterAddr; + } + } + + public void syncAll() { + this.syncTopicConfig(); + this.syncConsumerOffset(); + this.syncDelayOffset(); + this.syncSubscriptionGroupConfig(); + this.syncMessageRequestMode(); + + if (brokerController.getMessageStoreConfig().isTimerWheelEnable()) { + this.syncTimerMetrics(); + } + } + + private void syncTopicConfig() { + String masterAddrBak = this.masterAddr; + if (masterAddrBak != null && !masterAddrBak.equals(brokerController.getBrokerAddr())) { + try { + TopicConfigAndMappingSerializeWrapper topicWrapper = + this.brokerController.getBrokerOuterAPI().getAllTopicConfig(masterAddrBak); + TopicConfigManager topicConfigManager = this.brokerController.getTopicConfigManager(); + if (!topicConfigManager.getDataVersion().equals(topicWrapper.getDataVersion())) { + + topicConfigManager.getDataVersion().assignNewOne(topicWrapper.getDataVersion()); + + ConcurrentMap newTopicConfigTable = topicWrapper.getTopicConfigTable(); + ConcurrentMap topicConfigTable = topicConfigManager.getTopicConfigTable(); + + //delete + Iterator> iterator = topicConfigTable.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (!newTopicConfigTable.containsKey(entry.getKey())) { + iterator.remove(); + } + topicConfigManager.deleteTopicConfig(entry.getKey()); + } + + //update + newTopicConfigTable.values().forEach(topicConfigManager::putTopicConfig); + topicConfigManager.updateDataVersion(); + topicConfigManager.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(); + topicConfigTable.entrySet().removeIf(item -> !newTopicConfigTable.containsKey(item.getKey())); + //update + topicConfigTable.putAll(newTopicConfigTable); + + this.brokerController.getTopicQueueMappingManager().persist(); + } + LOGGER.info("Update slave topic config from master, {}", masterAddrBak); + } catch (Exception e) { + LOGGER.error("SyncTopicConfig Exception, {}", masterAddrBak, e); + } + } + } + + private void syncConsumerOffset() { + String masterAddrBak = this.masterAddr; + if (masterAddrBak != null && !masterAddrBak.equals(brokerController.getBrokerAddr())) { + try { + ConsumerOffsetSerializeWrapper offsetWrapper = + this.brokerController.getBrokerOuterAPI().getAllConsumerOffset(masterAddrBak); + this.brokerController.getConsumerOffsetManager().getOffsetTable() + .putAll(offsetWrapper.getOffsetTable()); + this.brokerController.getConsumerOffsetManager().getDataVersion().assignNewOne(offsetWrapper.getDataVersion()); + this.brokerController.getConsumerOffsetManager().persist(); + LOGGER.info("Update slave consumer offset from master, {}", masterAddrBak); + } catch (Exception e) { + LOGGER.error("SyncConsumerOffset Exception, {}", masterAddrBak, e); + } + } + } + + private void syncDelayOffset() { + String masterAddrBak = this.masterAddr; + if (masterAddrBak != null && !masterAddrBak.equals(brokerController.getBrokerAddr())) { + try { + String delayOffset = + this.brokerController.getBrokerOuterAPI().getAllDelayOffset(masterAddrBak); + if (delayOffset != null) { + + String fileName = + StorePathConfigHelper.getDelayOffsetStorePath(this.brokerController + .getMessageStoreConfig().getStorePathRootDir()); + try { + MixAll.string2File(delayOffset, fileName); + this.brokerController.getScheduleMessageService().loadWhenSyncDelayOffset(); + } catch (IOException e) { + LOGGER.error("Persist file Exception, {}", fileName, e); + } + } + LOGGER.info("Update slave delay offset from master, {}", masterAddrBak); + } catch (Exception e) { + LOGGER.error("SyncDelayOffset Exception, {}", masterAddrBak, e); + } + } + } + + private void syncSubscriptionGroupConfig() { + String masterAddrBak = this.masterAddr; + if (masterAddrBak != null && !masterAddrBak.equals(brokerController.getBrokerAddr())) { + try { + SubscriptionGroupWrapper subscriptionWrapper = + this.brokerController.getBrokerOuterAPI() + .getAllSubscriptionGroupConfig(masterAddrBak); + + if (!this.brokerController.getSubscriptionGroupManager().getDataVersion() + .equals(subscriptionWrapper.getDataVersion())) { + SubscriptionGroupManager subscriptionGroupManager = this.brokerController.getSubscriptionGroupManager(); + subscriptionGroupManager.getDataVersion().assignNewOne(subscriptionWrapper.getDataVersion()); + + ConcurrentMap curSubscriptionGroupTable = + subscriptionGroupManager.getSubscriptionGroupTable(); + ConcurrentMap newSubscriptionGroupTable = + subscriptionWrapper.getSubscriptionGroupTable(); + // delete + Iterator> iterator = curSubscriptionGroupTable.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry configEntry = iterator.next(); + if (!newSubscriptionGroupTable.containsKey(configEntry.getKey())) { + iterator.remove(); + } + subscriptionGroupManager.deleteSubscriptionGroupConfig(configEntry.getKey()); + } + // update + newSubscriptionGroupTable.values().forEach(subscriptionGroupManager::updateSubscriptionGroupConfigWithoutPersist); + // persist + subscriptionGroupManager.persist(); + 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(); + ConcurrentHashMap> curMessageRequestModeMap = + messageRequestModeManager.getMessageRequestModeMap(); + ConcurrentHashMap> newMessageRequestModeMap = + messageRequestModeSerializeWrapper.getMessageRequestModeMap(); + + // delete + curMessageRequestModeMap.entrySet().removeIf(e -> !newMessageRequestModeMap.containsKey(e.getKey())); + // update + curMessageRequestModeMap.putAll(newMessageRequestModeMap); + // persist + 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) { + 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 0000000..69e59fd --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/subscription/LmqSubscriptionGroupManager.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.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); + } + + @Override + public boolean containsSubscriptionGroup(String group) { + if (MixAll.isLmq(group)) { + return true; + } else { + return super.containsSubscriptionGroup(group); + } + } +} 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 new file mode 100644 index 0000000..f3e669f --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/subscription/SubscriptionGroupManager.java @@ -0,0 +1,410 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.HashMap; +import java.util.Iterator; +import java.util.List; +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.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.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.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; + +public class SubscriptionGroupManager extends ConfigManager { + protected static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + + protected ConcurrentMap subscriptionGroupTable = + new ConcurrentHashMap<>(1024); + + private ConcurrentMap> forbiddenTable = + new ConcurrentHashMap<>(4); + + protected final DataVersion dataVersion = new DataVersion(); + protected transient BrokerController brokerController; + + public SubscriptionGroupManager() { + this.init(); + } + + public SubscriptionGroupManager(BrokerController brokerController) { + this(brokerController, true); + } + + public SubscriptionGroupManager(BrokerController brokerController, boolean init) { + this.brokerController = brokerController; + if (init) { + init(); + } + } + + protected void init() { + { + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setGroupName(MixAll.TOOLS_CONSUMER_GROUP); + putSubscriptionGroupConfig(subscriptionGroupConfig); + } + + { + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setGroupName(MixAll.FILTERSRV_CONSUMER_GROUP); + putSubscriptionGroupConfig(subscriptionGroupConfig); + } + + { + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setGroupName(MixAll.SELF_TEST_CONSUMER_GROUP); + putSubscriptionGroupConfig(subscriptionGroupConfig); + } + + { + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setGroupName(MixAll.ONS_HTTP_PROXY_GROUP); + subscriptionGroupConfig.setConsumeBroadcastEnable(true); + putSubscriptionGroupConfig(subscriptionGroupConfig); + } + + { + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setGroupName(MixAll.CID_ONSAPI_PULL_GROUP); + subscriptionGroupConfig.setConsumeBroadcastEnable(true); + putSubscriptionGroupConfig(subscriptionGroupConfig); + } + + { + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setGroupName(MixAll.CID_ONSAPI_PERMISSION_GROUP); + subscriptionGroupConfig.setConsumeBroadcastEnable(true); + putSubscriptionGroupConfig(subscriptionGroupConfig); + } + + { + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setGroupName(MixAll.CID_ONSAPI_OWNER_GROUP); + subscriptionGroupConfig.setConsumeBroadcastEnable(true); + putSubscriptionGroupConfig(subscriptionGroupConfig); + } + + { + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setGroupName(MixAll.CID_SYS_RMQ_TRANS); + subscriptionGroupConfig.setConsumeBroadcastEnable(true); + putSubscriptionGroupConfig(subscriptionGroupConfig); + } + } + + public 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) { + updateSubscriptionGroupConfigWithoutPersist(config); + this.persist(); + } + + public void updateSubscriptionGroupConfigWithoutPersist(SubscriptionGroupConfig 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); + } + + updateDataVersion(); + } + + public void updateSubscriptionGroupConfigList(List configList) { + if (null == configList || configList.isEmpty()) { + return; + } + configList.forEach(this::updateSubscriptionGroupConfigWithoutPersist); + 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; + } + + protected 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); + } + + updateDataVersion(); + + this.persist(); + } + + public void disableConsume(final String groupName) { + SubscriptionGroupConfig old = getSubscriptionGroupConfig(groupName); + if (old != null) { + old.setConsumeEnable(false); + updateDataVersion(); + } + } + + public SubscriptionGroupConfig findSubscriptionGroupConfig(final String group) { + SubscriptionGroupConfig subscriptionGroupConfig = getSubscriptionGroupConfig(group); + if (null == subscriptionGroupConfig) { + if (brokerController.getBrokerConfig().isAutoCreateSubscriptionGroup() + || MixAll.isSysConsumerGroupAndEnableCreate(group, brokerController.getBrokerConfig().isEnableCreateSysGroup())) { + if (group.length() > Validators.CHARACTER_MAX_LENGTH || TopicValidator.isTopicOrGroupIllegal(group)) { + return null; + } + subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setGroupName(group); + SubscriptionGroupConfig preConfig = putSubscriptionGroupConfigIfAbsent(subscriptionGroupConfig); + if (null == preConfig) { + log.info("auto create a subscription group, {}", subscriptionGroupConfig.toString()); + } + updateDataVersion(); + this.persist(); + } + } + + return subscriptionGroupConfig; + } + + @Override + public String encode() { + return this.encode(false); + } + + @Override + public String configFilePath() { + return BrokerPathConfigHelper.getSubscriptionGroupPath(this.brokerController.getMessageStoreConfig() + .getStorePathRootDir()); + } + + @Override + public void decode(String jsonString) { + if (jsonString != null) { + 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); + } + + private void printLoadDataWhenFirstBoot(final SubscriptionGroupManager sgm) { + Iterator> it = sgm.getSubscriptionGroupTable().entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + log.info("load exist subscription group, {}", next.getValue().toString()); + } + } + + public ConcurrentMap getSubscriptionGroupTable() { + return subscriptionGroupTable; + } + + public ConcurrentMap> getForbiddenTable() { + return forbiddenTable; + } + + public void setForbiddenTable( + ConcurrentMap> forbiddenTable) { + this.forbiddenTable = forbiddenTable; + } + + public DataVersion getDataVersion() { + return dataVersion; + } + + public boolean loadDataVersion() { + String fileName = null; + try { + fileName = this.configFilePath(); + String jsonString = MixAll.file2String(fileName); + if (jsonString != null) { + SubscriptionGroupManager obj = RemotingSerializable.fromJson(jsonString, SubscriptionGroupManager.class); + if (obj != null) { + this.dataVersion.assignNewOne(obj.dataVersion); + this.printLoadDataWhenFirstBoot(obj); + log.info("load subGroup dataVersion success,{},{}", fileName, obj.dataVersion); + } + } + return true; + } catch (Exception e) { + log.error("load subGroup dataVersion failed" + fileName, e); + return false; + } + } + + public void deleteSubscriptionGroupConfig(final String groupName) { + SubscriptionGroupConfig old = removeSubscriptionGroupConfig(groupName); + this.forbiddenTable.remove(groupName); + if (old != null) { + log.info("delete subscription group OK, subscription group:{}", old); + updateDataVersion(); + 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; + } + } + } + + public void setDataVersion(DataVersion dataVersion) { + this.dataVersion.assignNewOne(dataVersion); + } + + public void updateDataVersion() { + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + dataVersion.nextVersion(stateMachineVersion); + } + +} 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 0000000..ca5a94a --- /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/TopicConfigManager.java b/broker/src/main/java/org/apache/rocketmq/broker/topic/TopicConfigManager.java new file mode 100644 index 0000000..b20cafc --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/topic/TopicConfigManager.java @@ -0,0 +1,743 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Iterator; +import java.util.List; +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.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.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.sysflag.TopicSysFlag; +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 org.apache.rocketmq.store.timer.TimerMessageStore; +import org.apache.rocketmq.tieredstore.TieredMessageStore; +import org.apache.rocketmq.tieredstore.metadata.MetadataStore; +import org.apache.rocketmq.tieredstore.metadata.entity.TopicMetadata; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class TopicConfigManager extends ConfigManager { + protected static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + private static final long LOCK_TIMEOUT_MILLIS = 3000; + private static final int SCHEDULE_TOPIC_QUEUE_NUM = 18; + + private transient final Lock topicConfigTableLock = new ReentrantLock(); + protected ConcurrentMap topicConfigTable = new ConcurrentHashMap<>(1024); + protected 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() { + { + String topic = TopicValidator.RMQ_SYS_SELF_TEST_TOPIC; + TopicConfig topicConfig = new TopicConfig(topic); + TopicValidator.addSystemTopic(topic); + topicConfig.setReadQueueNums(1); + topicConfig.setWriteQueueNums(1); + putTopicConfig(topicConfig); + } + { + if (this.brokerController.getBrokerConfig().isAutoCreateTopicEnable()) { + String topic = TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC; + TopicConfig topicConfig = new TopicConfig(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); + putTopicConfig(topicConfig); + } + } + { + String topic = TopicValidator.RMQ_SYS_BENCHMARK_TOPIC; + TopicConfig topicConfig = new TopicConfig(topic); + TopicValidator.addSystemTopic(topic); + topicConfig.setReadQueueNums(1024); + topicConfig.setWriteQueueNums(1024); + putTopicConfig(topicConfig); + } + { + String topic = this.brokerController.getBrokerConfig().getBrokerClusterName(); + TopicConfig topicConfig = new TopicConfig(topic); + TopicValidator.addSystemTopic(topic); + int perm = PermName.PERM_INHERIT; + if (this.brokerController.getBrokerConfig().isClusterTopicEnable()) { + perm |= PermName.PERM_READ | PermName.PERM_WRITE; + } + topicConfig.setPerm(perm); + putTopicConfig(topicConfig); + } + { + + String topic = this.brokerController.getBrokerConfig().getBrokerName(); + TopicConfig topicConfig = new TopicConfig(topic); + TopicValidator.addSystemTopic(topic); + int perm = PermName.PERM_INHERIT; + if (this.brokerController.getBrokerConfig().isBrokerTopicEnable()) { + perm |= PermName.PERM_READ | PermName.PERM_WRITE; + } + topicConfig.setReadQueueNums(1); + topicConfig.setWriteQueueNums(1); + topicConfig.setPerm(perm); + 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); + } + { + 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); + 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); + 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); + } + + { + if (this.brokerController.getMessageStoreConfig().isTimerWheelEnable()) { + String topic = TimerMessageStore.TIMER_TOPIC; + TopicConfig topicConfig = new TopicConfig(topic); + TopicValidator.addSystemTopic(topic); + topicConfig.setReadQueueNums(1); + topicConfig.setWriteQueueNums(1); + this.topicConfigTable.put(topicConfig.getTopicName(), topicConfig); + } + } + } + + public TopicConfig putTopicConfig(TopicConfig topicConfig) { + return this.topicConfigTable.put(topicConfig.getTopicName(), topicConfig); + } + + protected TopicConfig getTopicConfig(String topicName) { + return this.topicConfigTable.get(topicName); + } + + protected TopicConfig removeTopicConfig(String topicName) { + return this.topicConfigTable.remove(topicName); + } + + public TopicConfig selectTopicConfig(final String topic) { + return getTopicConfig(topic); + } + + public TopicConfig createTopicInSendMessageMethod(final String topic, final String defaultTopic, + final String remoteAddress, final int clientDefaultTopicQueueNums, final int topicSysFlag) { + TopicConfig topicConfig = null; + boolean createNew = false; + + try { + if (this.topicConfigTableLock.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { + try { + topicConfig = getTopicConfig(topic); + if (topicConfig != null) { + return topicConfig; + } + + TopicConfig defaultTopicConfig = getTopicConfig(defaultTopic); + if (defaultTopicConfig != null) { + if (defaultTopic.equals(TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC)) { + if (!this.brokerController.getBrokerConfig().isAutoCreateTopicEnable()) { + defaultTopicConfig.setPerm(PermName.PERM_READ | PermName.PERM_WRITE); + } + } + + if (PermName.isInherited(defaultTopicConfig.getPerm())) { + topicConfig = new TopicConfig(topic); + + int queueNums = Math.min(clientDefaultTopicQueueNums, defaultTopicConfig.getWriteQueueNums()); + + if (queueNums < 0) { + queueNums = 0; + } + + topicConfig.setReadQueueNums(queueNums); + topicConfig.setWriteQueueNums(queueNums); + int perm = defaultTopicConfig.getPerm(); + perm &= ~PermName.PERM_INHERIT; + topicConfig.setPerm(perm); + topicConfig.setTopicSysFlag(topicSysFlag); + topicConfig.setTopicFilterType(defaultTopicConfig.getTopicFilterType()); + } else { + log.warn("Create new topic failed, because the default topic[{}] has no perm [{}] producer:[{}]", + defaultTopic, defaultTopicConfig.getPerm(), remoteAddress); + } + } else { + log.warn("Create new topic failed, because the default topic[{}] not exist. producer:[{}]", + defaultTopic, remoteAddress); + } + + if (topicConfig != null) { + log.info("Create new topic by default topic:[{}] config:[{}] producer:[{}]", + defaultTopic, topicConfig, remoteAddress); + + putTopicConfig(topicConfig); + + updateDataVersion(); + + createNew = true; + + this.persist(); + } + } finally { + this.topicConfigTableLock.unlock(); + } + } + } catch (InterruptedException e) { + log.error("createTopicInSendMessageMethod exception", e); + } + + if (createNew) { + 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); + updateDataVersion(); + 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) { + 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.topicConfigTableLock.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { + try { + 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); + putTopicConfig(topicConfig); + createNew = true; + updateDataVersion(); + this.persist(); + } finally { + this.topicConfigTableLock.unlock(); + } + } + } catch (InterruptedException e) { + log.error("createTopicInSendMessageBackMethod exception", e); + } + + if (createNew) { + 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; + updateDataVersion(); + 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; + } + + public void updateTopicUnitFlag(final String topic, final boolean unit) { + + TopicConfig topicConfig = getTopicConfig(topic); + if (topicConfig != null) { + int oldTopicSysFlag = topicConfig.getTopicSysFlag(); + if (unit) { + topicConfig.setTopicSysFlag(TopicSysFlag.setUnitFlag(oldTopicSysFlag)); + } else { + topicConfig.setTopicSysFlag(TopicSysFlag.clearUnitFlag(oldTopicSysFlag)); + } + + log.info("update topic sys flag. oldTopicSysFlag={}, newTopicSysFlag={}", oldTopicSysFlag, + topicConfig.getTopicSysFlag()); + + putTopicConfig(topicConfig); + + updateDataVersion(); + + this.persist(); + registerBrokerData(topicConfig); + } + } + + public void updateTopicUnitSubFlag(final String topic, final boolean hasUnitSub) { + 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, + topicConfig.getTopicSysFlag()); + + putTopicConfig(topicConfig); + + updateDataVersion(); + + this.persist(); + registerBrokerData(topicConfig); + } + } + + public void updateSingleTopicConfigWithoutPersist(final TopicConfig 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); + updateTieredStoreTopicMetadata(topicConfig, newAttributes); + + TopicConfig old = putTopicConfig(topicConfig); + if (old != null) { + log.info("update topic config, old:[{}] new:[{}]", old, topicConfig); + } else { + log.info("create new topic [{}]", topicConfig); + } + + updateDataVersion(); + } + + public void updateTopicConfig(final TopicConfig topicConfig) { + updateSingleTopicConfigWithoutPersist(topicConfig); + this.persist(topicConfig.getTopicName(), topicConfig); + } + + public void updateTopicConfigList(final List topicConfigList) { + topicConfigList.forEach(this::updateSingleTopicConfigWithoutPersist); + this.persist(); + } + + private synchronized void updateTieredStoreTopicMetadata(final TopicConfig topicConfig, Map newAttributes) { + if (!(brokerController.getMessageStore() instanceof TieredMessageStore)) { + if (newAttributes.get(TopicAttributes.TOPIC_RESERVE_TIME_ATTRIBUTE.getName()) != null) { + throw new IllegalArgumentException("Update topic reserveTime not supported"); + } + return; + } + + String topic = topicConfig.getTopicName(); + long reserveTime = TopicAttributes.TOPIC_RESERVE_TIME_ATTRIBUTE.getDefaultValue(); + String attr = topicConfig.getAttributes().get(TopicAttributes.TOPIC_RESERVE_TIME_ATTRIBUTE.getName()); + if (attr != null) { + reserveTime = Long.parseLong(attr); + } + + log.info("Update tiered storage metadata, topic {}, reserveTime {}", topic, reserveTime); + TieredMessageStore tieredMessageStore = (TieredMessageStore) brokerController.getMessageStore(); + MetadataStore metadataStore = tieredMessageStore.getMetadataStore(); + TopicMetadata topicMetadata = metadataStore.getTopic(topic); + if (topicMetadata == null) { + metadataStore.addTopic(topic, reserveTime); + } else if (topicMetadata.getReserveTime() != reserveTime) { + topicMetadata.setReserveTime(reserveTime); + metadataStore.updateTopic(topicMetadata); + } + } + + public void updateOrderTopicConfig(final KVTable orderKVTableFromNs) { + + if (orderKVTableFromNs != null && orderKVTableFromNs.getTable() != null) { + boolean isChange = false; + Set orderTopics = orderKVTableFromNs.getTable().keySet(); + for (String topic : orderTopics) { + TopicConfig topicConfig = getTopicConfig(topic); + if (topicConfig != null && !topicConfig.isOrder()) { + topicConfig.setOrder(true); + isChange = true; + log.info("update order topic config, topic={}, order={}", topic, true); + } + } + + if (isChange) { + updateDataVersion(); + this.persist(); + } + } + } + + // make it testable + public Map allAttributes() { + return TopicAttributes.ALL; + } + + public boolean isOrderTopic(final String topic) { + TopicConfig topicConfig = getTopicConfig(topic); + if (topicConfig == null) { + return false; + } else { + return topicConfig.isOrder(); + } + } + + public void deleteTopicConfig(final String topic) { + TopicConfig old = removeTopicConfig(topic); + if (old != null) { + log.info("delete topic config OK, topic: {}", old); + updateDataVersion(); + this.persist(); + } else { + log.warn("delete topic config failed, topic: {} not exists", topic); + } + } + + public TopicConfigSerializeWrapper buildTopicConfigSerializeWrapper() { + TopicConfigSerializeWrapper topicConfigSerializeWrapper = new TopicConfigSerializeWrapper(); + topicConfigSerializeWrapper.setTopicConfigTable(this.topicConfigTable); + 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); + } + + public boolean loadDataVersion() { + String fileName = null; + try { + fileName = this.configFilePath(); + String jsonString = MixAll.file2String(fileName); + if (jsonString != null) { + TopicConfigSerializeWrapper topicConfigSerializeWrapper = + TopicConfigSerializeWrapper.fromJson(jsonString, TopicConfigSerializeWrapper.class); + if (topicConfigSerializeWrapper != null) { + this.dataVersion.assignNewOne(topicConfigSerializeWrapper.getDataVersion()); + log.info("load topic metadata dataVersion success {}, {}", fileName, topicConfigSerializeWrapper.getDataVersion()); + } + } + return true; + } catch (Exception e) { + log.error("load topic metadata dataVersion failed" + fileName, e); + return false; + } + } + + @Override + public String configFilePath() { + return BrokerPathConfigHelper.getTopicConfigPath(this.brokerController.getMessageStoreConfig().getStorePathRootDir()); + } + + @Override + public void decode(String jsonString) { + if (jsonString != null) { + TopicConfigSerializeWrapper topicConfigSerializeWrapper = + TopicConfigSerializeWrapper.fromJson(jsonString, TopicConfigSerializeWrapper.class); + if (topicConfigSerializeWrapper != null) { + this.topicConfigTable.putAll(topicConfigSerializeWrapper.getTopicConfigTable()); + this.dataVersion.assignNewOne(topicConfigSerializeWrapper.getDataVersion()); + this.printLoadDataWhenFirstBoot(topicConfigSerializeWrapper); + } + } + } + + public String encode(final boolean prettyFormat) { + TopicConfigSerializeWrapper topicConfigSerializeWrapper = new TopicConfigSerializeWrapper(); + topicConfigSerializeWrapper.setTopicConfigTable(this.topicConfigTable); + topicConfigSerializeWrapper.setDataVersion(this.dataVersion); + return topicConfigSerializeWrapper.toJson(prettyFormat); + } + + private void printLoadDataWhenFirstBoot(final TopicConfigSerializeWrapper tcs) { + Iterator> it = tcs.getTopicConfigTable().entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + log.info("load exist local topic, {}", next.getValue().toString()); + } + } + + 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); + } + + public void updateDataVersion() { + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + dataVersion.nextVersion(stateMachineVersion); + } + + + +} 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 0000000..c4fd4c6 --- /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 0000000..6b9cf15 --- /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 0000000..11bde5f --- /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 0000000..c8d49f4 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/transaction/AbstractTransactionalMessageCheckListener.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.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.setTopic(msgExt.getTopic()); + 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 0000000..d23b761 --- /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 0000000..28fff6a --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/transaction/TransactionMetrics.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.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.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +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)); + Path backupPath = Paths.get(backup); + try (FileChannel channel = FileChannel.open(backupPath, StandardOpenOption.WRITE)) { + channel.force(true); // force flush before deleting original file. + } + 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 0000000..948f9fb --- /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/TransactionalMessageCheckService.java b/broker/src/main/java/org/apache/rocketmq/broker/transaction/TransactionalMessageCheckService.java new file mode 100644 index 0000000..52209c3 --- /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 0000000..849e640 --- /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/queue/DefaultTransactionalMessageCheckListener.java b/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/DefaultTransactionalMessageCheckListener.java new file mode 100644 index 0000000..8e2b679 --- /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 0000000..a78970e --- /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 0000000..e8e5f13 --- /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 0000000..2383f4f --- /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 0000000..017803c --- /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; + } + Long removedOpOffset; + if ((removedOpOffset = removeMap.remove(i)) != null) { + log.debug("Half offset {} has been committed/rolled back", 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 { + Long tmpOpOffset; + if ((tmpOpOffset = removeMap.remove(prepareQueueOffset)) != null) { + 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 0000000..555ae4d --- /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 0000000..fb6e9e8 --- /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 0000000..dec4235 --- /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 0000000..8d92f43 --- /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 0000000..fd63ef1 --- /dev/null +++ b/broker/src/main/resources/rmq.broker.logback.xml @@ -0,0 +1,724 @@ + + + + + + + + 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 + + + + + + + + + + + brokerContainerLogDir + ${file.separator} + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}auth_audit.log + + true + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}otherdays${file.separator}auth_audit.%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/main/resources/transaction.sql b/broker/src/main/resources/transaction.sql new file mode 100644 index 0000000..881714c --- /dev/null +++ b/broker/src/main/resources/transaction.sql @@ -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. +-- +CREATE TABLE t_transaction( + offset NUMERIC(20) PRIMARY KEY, + producerGroup VARCHAR(64) +) diff --git a/broker/src/test/java/org/apache/rocketmq/broker/BrokerControllerTest.java b/broker/src/test/java/org/apache/rocketmq/broker/BrokerControllerTest.java new file mode 100644 index 0000000..3ce1fe3 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/BrokerControllerTest.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.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.RPCHook; +import org.apache.rocketmq.remoting.RemotingServer; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyRemotingAbstract; +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.pipeline.RequestPipeline; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BrokerControllerTest { + + 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(brokerConfig, nettyServerConfig, new NettyClientConfig(), messageStoreConfig); + assertThat(brokerController.initialize()).isTrue(); + brokerController.start(); + brokerController.shutdown(); + } + + @After + 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); + } + + @Test + public void testCustomRemotingServer() throws CloneNotSupportedException { + final RemotingServer mockRemotingServer = new NettyRemotingServer(nettyServerConfig); + final String mockRemotingServerName = "MOCK_REMOTING_SERVER"; + + BrokerController brokerController = new BrokerController(brokerConfig, nettyServerConfig, new NettyClientConfig(), messageStoreConfig); + brokerController.setRemotingServerByName(mockRemotingServerName, mockRemotingServer); + brokerController.initializeRemotingServer(); + + final RPCHook rpcHook = new RPCHook() { + @Override + public void doBeforeRequest(String remoteAddr, RemotingCommand request) { + + } + + @Override + public void doAfterResponse(String remoteAddr, RemotingCommand request, RemotingCommand response) { + + } + }; + brokerController.registerServerRPCHook(rpcHook); + + // setRequestPipelineTest + final RequestPipeline requestPipeline = (ctx, request) -> { + + }; + brokerController.setRequestPipeline(requestPipeline); + + NettyRemotingAbstract tcpRemotingServer = (NettyRemotingAbstract) brokerController.getRemotingServer(); + Assert.assertTrue(tcpRemotingServer.getRPCHook().contains(rpcHook)); + + NettyRemotingAbstract fastRemotingServer = (NettyRemotingAbstract) brokerController.getFastRemotingServer(); + Assert.assertTrue(fastRemotingServer.getRPCHook().contains(rpcHook)); + + NettyRemotingAbstract mockRemotingServer1 = (NettyRemotingAbstract) brokerController.getRemotingServerByName(mockRemotingServerName); + Assert.assertTrue(mockRemotingServer1.getRPCHook().contains(rpcHook)); + Assert.assertSame(mockRemotingServer, mockRemotingServer1); + } +} 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 0000000..766fcdd --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/BrokerOuterAPITest.java @@ -0,0 +1,426 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Channel; +import io.netty.channel.ChannelHandlerContext; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import io.netty.channel.DefaultChannelPromise; +import io.netty.util.concurrent.DefaultEventExecutor; +import org.apache.commons.lang3.tuple.Triple; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.broker.out.BrokerOuterAPI; +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.BrokerIdentity; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.remoting.common.SemaphoreReleaseOnlyOnce; +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.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.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.header.PullMessageResponseHeader; +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.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +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.doReturn; +import static org.mockito.Mockito.when; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(NettyRemotingClient.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(), new AuthConfig()); + 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 { + if (MixAll.isMac()) { + return; + } + 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 + 100); // Increase sleep time to force timeout + 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()); + } + + @Test + public void testPullMessageFromSpecificBrokerAsync_createChannel_null() throws Exception { + NettyRemotingClient mockClient = PowerMockito.spy(new NettyRemotingClient(new NettyClientConfig())); + PowerMockito.when(mockClient, "getAndCreateChannelAsync", any()).thenReturn(null); + BrokerOuterAPI api = new BrokerOuterAPI(new NettyClientConfig(), new AuthConfig()); + Field field = BrokerOuterAPI.class.getDeclaredField("remotingClient"); + field.setAccessible(true); + field.set(api, mockClient); + + Triple rst = api.pullMessageFromSpecificBrokerAsync("", "", "", "", 1, 1, 1, 3000L).join(); + Assert.assertNull(rst.getLeft()); + Assert.assertTrue(rst.getMiddle().contains("connect")); + Assert.assertTrue(rst.getRight()); // need retry + } + + @Test + public void testPullMessageFromSpecificBrokerAsync_createChannel_future_notSuccess() throws Exception { + NettyRemotingClient mockClient = PowerMockito.spy(new NettyRemotingClient(new NettyClientConfig())); + DefaultChannelPromise promise = PowerMockito.spy(new DefaultChannelPromise(PowerMockito.mock(Channel.class), new DefaultEventExecutor())); + PowerMockito.when(mockClient, "getAndCreateChannelAsync", any()).thenReturn(promise); + BrokerOuterAPI api = new BrokerOuterAPI(new NettyClientConfig(), new AuthConfig()); + Field field = BrokerOuterAPI.class.getDeclaredField("remotingClient"); + field.setAccessible(true); + field.set(api, mockClient); + + promise.tryFailure(new Throwable()); + Triple rst + = api.pullMessageFromSpecificBrokerAsync("", "", "", "", 1, 1, 1, 3000L).join(); + Assert.assertNull(rst.getLeft()); + Assert.assertTrue(rst.getMiddle().contains("connect")); + Assert.assertTrue(rst.getRight()); // need retry + } + + // skip other future status test + + @Test + public void testPullMessageFromSpecificBrokerAsync_timeout() throws Exception { + Channel channel = Mockito.mock(Channel.class); + when(channel.isActive()).thenReturn(true); + NettyRemotingClient mockClient = PowerMockito.spy(new NettyRemotingClient(new NettyClientConfig())); + DefaultChannelPromise promise = PowerMockito.spy(new DefaultChannelPromise(PowerMockito.mock(Channel.class), new DefaultEventExecutor())); + PowerMockito.when(mockClient, "getAndCreateChannelAsync", any()).thenReturn(promise); + when(promise.channel()).thenReturn(channel); + BrokerOuterAPI api = new BrokerOuterAPI(new NettyClientConfig(), new AuthConfig()); + Field field = BrokerOuterAPI.class.getDeclaredField("remotingClient"); + field.setAccessible(true); + field.set(api, mockClient); + + CompletableFuture future = new CompletableFuture<>(); + doReturn(future).when(mockClient).invokeImpl(any(Channel.class), any(RemotingCommand.class), anyLong()); + promise.trySuccess(null); + future.completeExceptionally(new RemotingTimeoutException("wait response on the channel timeout")); + Triple rst = api.pullMessageFromSpecificBrokerAsync("", "", "", "", 1, 1, 1, 3000L).join(); + Assert.assertNull(rst.getLeft()); + Assert.assertTrue(rst.getMiddle().contains("timeout")); + Assert.assertTrue(rst.getRight()); // need retry + } + + @Test + public void testPullMessageFromSpecificBrokerAsync_brokerReturn_pullStatusCode() throws Exception { + Channel channel = Mockito.mock(Channel.class); + when(channel.isActive()).thenReturn(true); + NettyRemotingClient mockClient = PowerMockito.spy(new NettyRemotingClient(new NettyClientConfig())); + DefaultChannelPromise promise = PowerMockito.spy(new DefaultChannelPromise(PowerMockito.mock(Channel.class), new DefaultEventExecutor())); + PowerMockito.when(mockClient, "getAndCreateChannelAsync", any()).thenReturn(promise); + when(promise.channel()).thenReturn(channel); + BrokerOuterAPI api = new BrokerOuterAPI(new NettyClientConfig(), new AuthConfig()); + Field field = BrokerOuterAPI.class.getDeclaredField("remotingClient"); + field.setAccessible(true); + field.set(api, mockClient); + + int[] respCodes = new int[] {ResponseCode.SUCCESS, ResponseCode.PULL_NOT_FOUND, ResponseCode.PULL_RETRY_IMMEDIATELY, ResponseCode.PULL_OFFSET_MOVED}; + PullStatus[] respStatus = new PullStatus[] {PullStatus.FOUND, PullStatus.NO_NEW_MSG, PullStatus.NO_MATCHED_MSG, PullStatus.OFFSET_ILLEGAL}; + for (int i = 0; i < respCodes.length; i++) { + CompletableFuture future = new CompletableFuture<>(); + doReturn(future).when(mockClient).invokeImpl(any(Channel.class), any(RemotingCommand.class), anyLong()); + RemotingCommand response = mockPullMessageResponse(respCodes[i]); + ResponseFuture responseFuture = new ResponseFuture(channel, 0, null, 1000, + resp -> { }, new SemaphoreReleaseOnlyOnce(new Semaphore(1))); + responseFuture.setResponseCommand(response); + promise.trySuccess(null); + future.complete(responseFuture); + + Triple rst = api.pullMessageFromSpecificBrokerAsync("", "", "", "", 1, 1, 1, 3000L).join(); + Assert.assertEquals(respStatus[i], rst.getLeft().getPullStatus()); + if (ResponseCode.SUCCESS == respCodes[i]) { + Assert.assertEquals(1, rst.getLeft().getMsgFoundList().size()); + } else { + Assert.assertNull(rst.getLeft().getMsgFoundList()); + } + Assert.assertEquals(respStatus[i].name(), rst.getMiddle()); + Assert.assertFalse(rst.getRight()); // no retry + } + } + + @Test + public void testPullMessageFromSpecificBrokerAsync_brokerReturn_allOtherResponseCode() throws Exception { + Channel channel = Mockito.mock(Channel.class); + when(channel.isActive()).thenReturn(true); + NettyRemotingClient mockClient = PowerMockito.spy(new NettyRemotingClient(new NettyClientConfig())); + DefaultChannelPromise promise = PowerMockito.spy(new DefaultChannelPromise(PowerMockito.mock(Channel.class), new DefaultEventExecutor())); + PowerMockito.when(mockClient, "getAndCreateChannelAsync", any()).thenReturn(promise); + when(promise.channel()).thenReturn(channel); + BrokerOuterAPI api = new BrokerOuterAPI(new NettyClientConfig(), new AuthConfig()); + Field field = BrokerOuterAPI.class.getDeclaredField("remotingClient"); + field.setAccessible(true); + field.set(api, mockClient); + + CompletableFuture future = new CompletableFuture<>(); + doReturn(future).when(mockClient).invokeImpl(any(Channel.class), any(RemotingCommand.class), anyLong()); + // test one code here, skip others + RemotingCommand response = mockPullMessageResponse(ResponseCode.SUBSCRIPTION_NOT_EXIST); + ResponseFuture responseFuture = new ResponseFuture(channel, 0, null, 1000, + resp -> { }, new SemaphoreReleaseOnlyOnce(new Semaphore(1))); + responseFuture.setResponseCommand(response); + promise.trySuccess(null); + future.complete(responseFuture); + + Triple rst = api.pullMessageFromSpecificBrokerAsync("", "", "", "", 1, 1, 1, 3000L).join(); + Assert.assertNull(rst.getLeft()); + Assert.assertTrue(rst.getMiddle().contains(ResponseCode.SUBSCRIPTION_NOT_EXIST + "")); + Assert.assertTrue(rst.getRight()); // need retry + } + + private RemotingCommand mockPullMessageResponse(int responseCode) throws Exception { + RemotingCommand response = RemotingCommand.createResponseCommand(PullMessageResponseHeader.class); + response.setCode(responseCode); + if (responseCode == ResponseCode.SUCCESS) { + MessageExt msg = new MessageExt(); + msg.setBody("HW".getBytes()); + msg.setTopic("topic"); + msg.setBornHost(new InetSocketAddress("127.0.0.1", 9000)); + msg.setStoreHost(new InetSocketAddress("127.0.0.1", 9000)); + byte[] encode = MessageDecoder.encode(msg, false); + response.setBody(encode); + } + PullMessageResponseHeader responseHeader = (PullMessageResponseHeader) response.readCustomHeader(); + responseHeader.setNextBeginOffset(0L); + responseHeader.setMaxOffset(0L); + responseHeader.setMinOffset(0L); + responseHeader.setOffsetDelta(0L); + responseHeader.setTopicSysFlag(0); + responseHeader.setGroupSysFlag(0); + responseHeader.setSuggestWhichBrokerId(0L); + responseHeader.setForbiddenType(0); + response.makeCustomHeaderToNet(); + return response; + } + +} 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 0000000..61a0891 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/BrokerPathConfigHelperTest.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; + +import java.io.File; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class BrokerPathConfigHelperTest { + + @Test + public void testGetPath() { + 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); + + String topicQueueMappingPath = BrokerPathConfigHelper.getTopicQueueMappingPath("/home/admin/store".replace("/", File.separator)); + assertEquals("/home/admin/store/config/topicQueueMapping.json".replace("/", File.separator), topicQueueMappingPath); + + String consumerOrderInfoPath = BrokerPathConfigHelper.getConsumerOrderInfoPath("/home/admin/store".replace("/", File.separator)); + assertEquals("/home/admin/store/config/consumerOrderInfo.json".replace("/", File.separator), consumerOrderInfoPath); + + String timercheckPath = BrokerPathConfigHelper.getTimerCheckPath("/home/admin/store".replace("/", File.separator)); + assertEquals("/home/admin/store/config/timercheck".replace("/", File.separator), timercheckPath); + + String timermetricsPath = BrokerPathConfigHelper.getTimerMetricsPath("/home/admin/store".replace("/", File.separator)); + assertEquals("/home/admin/store/config/timermetrics".replace("/", File.separator), timermetricsPath); + + String transactionMetricsPath = BrokerPathConfigHelper.getTransactionMetricsPath("/home/admin/store".replace("/", File.separator)); + assertEquals("/home/admin/store/config/transactionMetrics".replace("/", File.separator), transactionMetricsPath); + + String consumerFilterPath = BrokerPathConfigHelper.getConsumerFilterPath("/home/admin/store".replace("/", File.separator)); + assertEquals("/home/admin/store/config/consumerFilter.json".replace("/", File.separator), consumerFilterPath); + + String messageRequestModePath = BrokerPathConfigHelper.getMessageRequestModePath("/home/admin/store".replace("/", File.separator)); + assertEquals("/home/admin/store/config/messageRequestMode.json".replace("/", File.separator), messageRequestModePath); + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/BrokerStartupTest.java b/broker/src/test/java/org/apache/rocketmq/broker/BrokerStartupTest.java new file mode 100644 index 0000000..ce370a3 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/BrokerStartupTest.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; + +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; + +public class BrokerStartupTest { + + private String storePathRootDir = "."; + + @Test + public void testProperties2SystemEnv() throws NoSuchMethodException, InvocationTargetException, + IllegalAccessException { + Properties properties = new Properties(); + Class clazz = BrokerStartup.class; + Method method = clazz.getDeclaredMethod("properties2SystemEnv", Properties.class); + method.setAccessible(true); + { + 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 0000000..d190c0d --- /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 0000000..a23ad20 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/client/ConsumerManagerTest.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.client; + +import io.netty.channel.Channel; +import org.apache.rocketmq.broker.BrokerController; +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.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.HashSet; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +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.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ConsumerManagerTest { + + private ClientChannelInfo clientChannelInfo; + + @Mock + private Channel channel; + + private ConsumerManager consumerManager; + + @Mock + private BrokerController brokerController; + + private final BrokerConfig brokerConfig = new BrokerConfig(); + + 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 defaultConsumerIdsChangeListener = new DefaultConsumerIdsChangeListener(brokerController); + BrokerStatsManager brokerStatsManager = new BrokerStatsManager(brokerConfig); + consumerManager = spy(new ConsumerManager(defaultConsumerIdsChangeListener, brokerStatsManager, brokerConfig)); + ConsumerFilterManager consumerFilterManager = mock(ConsumerFilterManager.class); + when(brokerController.getConsumerFilterManager()).thenReturn(consumerFilterManager); + } + + @Test + public void compensateBasicConsumerInfoTest() { + ConsumerGroupInfo consumerGroupInfo = consumerManager.getConsumerGroupInfo(GROUP, true); + assertThat(consumerGroupInfo).isNull(); + + consumerManager.compensateBasicConsumerInfo(GROUP, ConsumeType.CONSUME_ACTIVELY, MessageModel.BROADCASTING); + consumerGroupInfo = consumerManager.getConsumerGroupInfo(GROUP, true); + assertThat(consumerGroupInfo).isNotNull(); + assertThat(consumerGroupInfo.getConsumeType()).isEqualTo(ConsumeType.CONSUME_ACTIVELY); + assertThat(consumerGroupInfo.getMessageModel()).isEqualTo(MessageModel.BROADCASTING); + } + + @Test + public void compensateSubscribeDataTest() { + ConsumerGroupInfo consumerGroupInfo = consumerManager.getConsumerGroupInfo(GROUP, true); + assertThat(consumerGroupInfo).isNull(); + + consumerManager.compensateSubscribeData(GROUP, TOPIC, new SubscriptionData(TOPIC, SubscriptionData.SUB_ALL)); + consumerGroupInfo = consumerManager.getConsumerGroupInfo(GROUP, true); + assertThat(consumerGroupInfo).isNotNull(); + assertThat(consumerGroupInfo.getSubscriptionTable().size()).isEqualTo(1); + SubscriptionData subscriptionData = consumerGroupInfo.getSubscriptionTable().get(TOPIC); + assertThat(subscriptionData).isNotNull(); + assertThat(subscriptionData.getTopic()).isEqualTo(TOPIC); + 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); + verify(consumerManager, never()).callConsumerIdsChangeListener(eq(ConsumerGroupEvent.CHANGE), any(), any()); + assertThat(consumerManager.getConsumerTable().get(GROUP)).isNotNull(); + } + + @Test + public void unregisterConsumerTest() { + // register + register(); + + // unregister + consumerManager.unregisterConsumer(GROUP, clientChannelInfo, true); + verify(consumerManager, never()).callConsumerIdsChangeListener(eq(ConsumerGroupEvent.CHANGE), any(), any()); + assertThat(consumerManager.getConsumerTable().get(GROUP)).isNull(); + } + + @Test + public void findChannelTest() { + register(); + final ClientChannelInfo consumerManagerChannel = consumerManager.findChannel(GROUP, CLIENT_ID); + assertThat(consumerManagerChannel).isNotNull(); + } + + @Test + public void findSubscriptionDataTest() { + register(); + final SubscriptionData subscriptionData = consumerManager.findSubscriptionData(GROUP, TOPIC); + assertThat(subscriptionData).isNotNull(); + } + + @Test + public void findSubscriptionDataCountTest() { + register(); + final int count = consumerManager.findSubscriptionDataCount(GROUP); + assertTrue(count > 0); + } + + @Test + public void findSubscriptionTest() { + SubscriptionData subscriptionData = consumerManager.findSubscriptionData(GROUP, TOPIC, true); + assertThat(subscriptionData).isNull(); + + consumerManager.compensateSubscribeData(GROUP, TOPIC, new SubscriptionData(TOPIC, SubscriptionData.SUB_ALL)); + subscriptionData = consumerManager.findSubscriptionData(GROUP, TOPIC, true); + assertThat(subscriptionData).isNotNull(); + assertThat(subscriptionData.getTopic()).isEqualTo(TOPIC); + assertThat(subscriptionData.getSubString()).isEqualTo(SubscriptionData.SUB_ALL); + + subscriptionData = consumerManager.findSubscriptionData(GROUP, TOPIC, false); + assertThat(subscriptionData).isNull(); + } + + @Test + public void scanNotActiveChannelTest() { + clientChannelInfo.setLastUpdateTimestamp(System.currentTimeMillis() - brokerConfig.getChannelExpiredTimeout() * 2); + consumerManager.scanNotActiveChannel(); + assertThat(consumerManager.getConsumerTable().size()).isEqualTo(0); + } + + @Test + public void queryTopicConsumeByWhoTest() { + register(); + final HashSet consumeGroup = consumerManager.queryTopicConsumeByWho(TOPIC); + assertFalse(consumeGroup.isEmpty()); + } + + @Test + public void doChannelCloseEventTest() { + consumerManager.doChannelCloseEvent("127.0.0.1", channel); + verify(consumerManager, never()).callConsumerIdsChangeListener(eq(ConsumerGroupEvent.CHANGE), any(), any()); + assertEquals(0, consumerManager.getConsumerTable().size()); + } + + 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(); + assertThat(consumerManager.getConsumerGroupInfo(GROUP, true)).isNotNull(); + assertThat(consumerManager.findSubscriptionData(GROUP, TOPIC)).isNull(); + 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 new file mode 100644 index 0000000..451b0e0 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/client/ProducerManagerTest.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.broker.client; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import java.lang.reflect.Field; +import java.util.Map; + +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.rocketmq.common.BrokerConfig; +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; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ProducerManagerTest { + + private BrokerConfig brokerConfig; + private ProducerManager producerManager; + private String group = "FooBar"; + private ClientChannelInfo clientInfo; + + @Mock + private Channel channel; + + @Before + public void init() { + brokerConfig = new BrokerConfig(); + producerManager = new ProducerManager(null, brokerConfig); + 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 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(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)).isNull(); + assertThat(groupRef.get()).isEqualTo(group); + assertThat(clientChannelInfoRef.get()).isSameAs(clientInfo); + assertThat(producerManager.findChannel("clientId")).isNull(); + } + + @Test + public void testRegisterProducer() { + brokerConfig.setEnableRegisterProducer(false); + brokerConfig.setRejectTransactionMessage(true); + producerManager.registerProducer(group, clientInfo); + Map channelMap = producerManager.getGroupChannelTable().get(group); + Channel channel1 = producerManager.findChannel("clientId"); + assertThat(channelMap).isNull(); + assertThat(channel1).isNull(); + + brokerConfig.setEnableRegisterProducer(true); + brokerConfig.setRejectTransactionMessage(false); + producerManager.registerProducer(group, clientInfo); + channelMap = producerManager.getGroupChannelTable().get(group); + 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); + 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/client/net/Broker2ClientTest.java b/broker/src/test/java/org/apache/rocketmq/broker/client/net/Broker2ClientTest.java new file mode 100644 index 0000000..ccb489a --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/client/net/Broker2ClientTest.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.broker.client.net; + +import io.netty.channel.Channel; +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.offset.ConsumerOffsetManager; +import org.apache.rocketmq.broker.topic.TopicConfigManager; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.remoting.RemotingServer; +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.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.GetConsumerStatusBody; +import org.apache.rocketmq.remoting.protocol.header.CheckTransactionStateRequestHeader; +import org.apache.rocketmq.store.MessageStore; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class Broker2ClientTest { + + @Mock + private BrokerController brokerController; + + @Mock + private RemotingServer remotingServer; + + @Mock + private ConsumerManager consumerManager; + + @Mock + private TopicConfigManager topicConfigManager; + + @Mock + private ConsumerOffsetManager consumerOffsetManager; + + @Mock + private Channel channel; + + @Mock + private ConsumerGroupInfo consumerGroupInfo; + + private Broker2Client broker2Client; + + private final String defaultTopic = "defaultTopic"; + + private final String defaultBroker = "defaultBroker"; + + private final String defaultGroup = "defaultGroup"; + + private final long timestamp = System.currentTimeMillis(); + + private final boolean isForce = true; + + @Before + public void init() { + broker2Client = new Broker2Client(brokerController); + when(brokerController.getRemotingServer()).thenReturn(remotingServer); + when(brokerController.getTopicConfigManager()).thenReturn(topicConfigManager); + when(brokerController.getConsumerManager()).thenReturn(consumerManager); + when(brokerController.getConsumerOffsetManager()).thenReturn(consumerOffsetManager); + when(brokerController.getBrokerConfig()).thenReturn(mock(BrokerConfig.class)); + when(brokerController.getMessageStore()).thenReturn(mock(MessageStore.class)); + when(consumerManager.getConsumerGroupInfo(any())).thenReturn(consumerGroupInfo); + } + + @Test + public void testCheckProducerTransactionState() throws Exception { + CheckTransactionStateRequestHeader requestHeader = new CheckTransactionStateRequestHeader(); + broker2Client.checkProducerTransactionState("group", channel, requestHeader, createMessageExt()); + verify(remotingServer).invokeOneway(eq(channel), any(RemotingCommand.class), eq(10L)); + } + + @Test + public void testCheckProducerTransactionStateException() throws Exception { + CheckTransactionStateRequestHeader requestHeader = new CheckTransactionStateRequestHeader(); + MessageExt messageExt = createMessageExt(); + doThrow(new RuntimeException("Test Exception")) + .when(remotingServer) + .invokeOneway(any(Channel.class), + any(RemotingCommand.class), + anyLong()); + broker2Client.checkProducerTransactionState("group", channel, requestHeader, messageExt); + verify(brokerController.getRemotingServer()).invokeOneway(eq(channel), any(RemotingCommand.class), eq(10L)); + } + + @Test + public void testResetOffsetNoTopicConfig() throws RemotingCommandException { + when(topicConfigManager.selectTopicConfig(defaultTopic)).thenReturn(null); + RemotingCommand response = broker2Client.resetOffset(defaultTopic, defaultGroup, timestamp, isForce); + assertEquals(ResponseCode.TOPIC_NOT_EXIST, response.getCode()); + } + + @Test + public void testResetOffsetNoConsumerGroupInfo() throws RemotingCommandException { + TopicConfig topicConfig = mock(TopicConfig.class); + when(topicConfigManager.selectTopicConfig(defaultTopic)).thenReturn(topicConfig); + when(topicConfig.getWriteQueueNums()).thenReturn(1); + when(consumerOffsetManager.queryOffset(defaultGroup, defaultTopic, 0)).thenReturn(0L); + RemotingCommand response = broker2Client.resetOffset(defaultTopic, defaultGroup, timestamp, isForce); + assertEquals(ResponseCode.CONSUMER_NOT_ONLINE, response.getCode()); + } + + @Test + public void testResetOffset() throws RemotingCommandException { + TopicConfig topicConfig = mock(TopicConfig.class); + when(topicConfigManager.selectTopicConfig(defaultTopic)).thenReturn(topicConfig); + when(topicConfig.getWriteQueueNums()).thenReturn(1); + when(brokerController.getConsumerOffsetManager().queryOffset(defaultGroup, defaultTopic, 0)).thenReturn(0L); + BrokerConfig brokerConfig = mock(BrokerConfig.class); + when(brokerController.getBrokerConfig()).thenReturn(brokerConfig); + when(brokerConfig.getBrokerName()).thenReturn(defaultBroker); + ConsumerGroupInfo consumerGroupInfo = mock(ConsumerGroupInfo.class); + when(consumerManager.getConsumerGroupInfo(defaultGroup)).thenReturn(consumerGroupInfo); + RemotingCommand response = broker2Client.resetOffset(defaultTopic, defaultGroup, timestamp, isForce); + assertEquals(ResponseCode.CONSUMER_NOT_ONLINE, response.getCode()); + } + + @Test + public void testGetConsumeStatusNoConsumerOnline() { + when(consumerGroupInfo.getChannelInfoTable()).thenReturn(new ConcurrentHashMap<>()); + RemotingCommand response = broker2Client.getConsumeStatus(defaultTopic, defaultGroup, ""); + assertEquals(ResponseCode.SYSTEM_ERROR, response.getCode()); + } + + @Test + public void testGetConsumeStatusClientDoesNotSupportFeature() { + ClientChannelInfo clientChannelInfo = new ClientChannelInfo(channel, "defaultClientId", null, MQVersion.Version.V3_0_6.ordinal()); + ConcurrentMap channelInfoTable = new ConcurrentHashMap<>(); + channelInfoTable.put(channel, clientChannelInfo); + when(consumerGroupInfo.getChannelInfoTable()).thenReturn(channelInfoTable); + RemotingCommand response = broker2Client.getConsumeStatus(defaultTopic, defaultGroup, ""); + assertEquals(ResponseCode.SYSTEM_ERROR, response.getCode()); + } + + @Test + public void testGetConsumeStatus() throws Exception { + ConcurrentMap channelInfoTable = new ConcurrentHashMap<>(); + ClientChannelInfo clientChannelInfo = mock(ClientChannelInfo.class); + when(clientChannelInfo.getVersion()).thenReturn(MQVersion.CURRENT_VERSION); + channelInfoTable.put(channel, clientChannelInfo); + when(consumerGroupInfo.getChannelInfoTable()).thenReturn(channelInfoTable); + RemotingCommand responseMock = mock(RemotingCommand.class); + when(responseMock.getCode()).thenReturn(ResponseCode.SUCCESS); + when(responseMock.getBody()).thenReturn("{\"consumerTable\":{}}".getBytes(StandardCharsets.UTF_8)); + when(remotingServer.invokeSync(any(Channel.class), any(RemotingCommand.class), anyLong())).thenReturn(responseMock); + RemotingCommand response = broker2Client.getConsumeStatus(defaultTopic, defaultGroup, ""); + assertEquals(ResponseCode.SUCCESS, response.getCode()); + GetConsumerStatusBody body = RemotingSerializable.decode(response.getBody(), GetConsumerStatusBody.class); + assertEquals(1, body.getConsumerTable().size()); + } + + private MessageExt createMessageExt() { + MessageExt result = new MessageExt(); + result.setBody("body".getBytes(StandardCharsets.UTF_8)); + result.setTopic(defaultTopic); + result.setBrokerName(defaultBroker); + result.putUserProperty("key", "value"); + result.getProperties().put(MessageConst.PROPERTY_PRODUCER_GROUP, defaultGroup); + result.getProperties().put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, "TX1"); + result.setKeys("keys"); + SocketAddress bornHost = new InetSocketAddress("127.0.0.1", 12911); + SocketAddress storeHost = new InetSocketAddress("127.0.0.1", 10911); + result.setStoreHost(storeHost); + result.setBornHost(bornHost); + return result; + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/client/rebalance/RebalanceLockManagerTest.java b/broker/src/test/java/org/apache/rocketmq/broker/client/rebalance/RebalanceLockManagerTest.java new file mode 100644 index 0000000..e231d61 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/client/rebalance/RebalanceLockManagerTest.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.broker.client.rebalance; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.common.message.MessageQueue; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class RebalanceLockManagerTest { + + @Mock + private RebalanceLockManager.LockEntry lockEntry; + + private final RebalanceLockManager rebalanceLockManager = new RebalanceLockManager(); + + private final String defaultTopic = "defaultTopic"; + + private final String defaultBroker = "defaultBroker"; + + private final String defaultGroup = "defaultGroup"; + + private final String defaultClientId = "defaultClientId"; + + @Test + public void testIsLockAllExpiredGroupNotExist() { + assertTrue(rebalanceLockManager.isLockAllExpired(defaultGroup)); + } + + @Test + public void testIsLockAllExpiredGroupExist() throws IllegalAccessException { + FieldUtils.writeDeclaredField(rebalanceLockManager, "mqLockTable", createMQLockTable(), true); + when(lockEntry.isExpired()).thenReturn(false); + assertFalse(rebalanceLockManager.isLockAllExpired(defaultGroup)); + } + + @Test + public void testIsLockAllExpiredGroupExistSomeExpired() throws IllegalAccessException { + FieldUtils.writeDeclaredField(rebalanceLockManager, "mqLockTable", createMQLockTable(), true); + when(lockEntry.isExpired()).thenReturn(true).thenReturn(false); + assertFalse(rebalanceLockManager.isLockAllExpired(defaultGroup)); + } + + @Test + public void testTryLockNotLocked() { + assertTrue(rebalanceLockManager.tryLock(defaultGroup, createDefaultMessageQueue(), defaultClientId)); + } + + @Test + public void testTryLockSameClient() throws IllegalAccessException { + when(lockEntry.isLocked(defaultClientId)).thenReturn(true); + FieldUtils.writeDeclaredField(rebalanceLockManager, "mqLockTable", createMQLockTable(), true); + assertTrue(rebalanceLockManager.tryLock(defaultGroup, createDefaultMessageQueue(), defaultClientId)); + } + + @Test + public void testTryLockDifferentClient() throws Exception { + when(lockEntry.isLocked(defaultClientId)).thenReturn(false); + FieldUtils.writeDeclaredField(rebalanceLockManager, "mqLockTable", createMQLockTable(), true); + assertFalse(rebalanceLockManager.tryLock(defaultGroup, createDefaultMessageQueue(), defaultClientId)); + } + + @Test + public void testTryLockButExpired() throws IllegalAccessException { + when(lockEntry.isExpired()).thenReturn(true); + FieldUtils.writeDeclaredField(rebalanceLockManager, "mqLockTable", createMQLockTable(), true); + assertTrue(rebalanceLockManager.tryLock(defaultGroup, createDefaultMessageQueue(), defaultClientId)); + } + + @Test + public void testTryLockBatchAllLocked() { + Set mqs = createMessageQueue(2); + Set actual = rebalanceLockManager.tryLockBatch(defaultGroup, mqs, defaultClientId); + assertEquals(mqs, actual); + } + + @Test + public void testTryLockBatchNoneLocked() throws IllegalAccessException { + when(lockEntry.isLocked(defaultClientId)).thenReturn(false); + FieldUtils.writeDeclaredField(rebalanceLockManager, "mqLockTable", createMQLockTable(), true); + Set actual = rebalanceLockManager.tryLockBatch(defaultGroup, createMessageQueue(2), defaultClientId); + assertTrue(actual.isEmpty()); + } + + @Test + public void testTryLockBatchSomeLocked() throws IllegalAccessException { + Set mqs = new HashSet<>(); + MessageQueue mq1 = new MessageQueue(defaultTopic, defaultBroker, 0); + MessageQueue mq2 = new MessageQueue(defaultTopic, defaultBroker, 1); + mqs.add(mq1); + mqs.add(mq2); + when(lockEntry.isLocked(defaultClientId)).thenReturn(true).thenReturn(false); + FieldUtils.writeDeclaredField(rebalanceLockManager, "mqLockTable", createMQLockTable(), true); + Set actual = rebalanceLockManager.tryLockBatch(defaultGroup, mqs, defaultClientId); + Set expected = new HashSet<>(); + expected.add(mq2); + assertEquals(expected, actual); + } + + @Test + public void testUnlockBatch() throws IllegalAccessException { + when(lockEntry.getClientId()).thenReturn(defaultClientId); + ConcurrentMap> mqLockTable = createMQLockTable(); + FieldUtils.writeDeclaredField(rebalanceLockManager, "mqLockTable", mqLockTable, true); + rebalanceLockManager.unlockBatch(defaultGroup, createMessageQueue(1), defaultClientId); + assertEquals(1, mqLockTable.get(defaultGroup).values().size()); + } + + @Test + public void testUnlockBatchByOtherClient() throws IllegalAccessException { + when(lockEntry.getClientId()).thenReturn("otherClientId"); + ConcurrentMap> mqLockTable = createMQLockTable(); + FieldUtils.writeDeclaredField(rebalanceLockManager, "mqLockTable", mqLockTable, true); + rebalanceLockManager.unlockBatch(defaultGroup, createMessageQueue(1), defaultClientId); + assertEquals(2, mqLockTable.get(defaultGroup).values().size()); + } + + private MessageQueue createDefaultMessageQueue() { + return createMessageQueue(1).iterator().next(); + } + + private Set createMessageQueue(final int count) { + Set result = new HashSet<>(); + for (int i = 0; i < count; i++) { + result.add(new MessageQueue(defaultTopic, defaultBroker, i)); + } + return result; + } + + private ConcurrentMap> createMQLockTable() { + MessageQueue messageQueue1 = new MessageQueue(defaultTopic, defaultBroker, 0); + MessageQueue messageQueue2 = new MessageQueue(defaultTopic, defaultBroker, 1); + ConcurrentHashMap lockEntryMap = new ConcurrentHashMap<>(); + lockEntryMap.put(messageQueue1, lockEntry); + lockEntryMap.put(messageQueue2, lockEntry); + ConcurrentMap> result = new ConcurrentHashMap<>(); + result.put(defaultGroup, lockEntryMap); + return result; + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/coldctr/ColdDataCgCtrServiceTest.java b/broker/src/test/java/org/apache/rocketmq/broker/coldctr/ColdDataCgCtrServiceTest.java new file mode 100644 index 0000000..7ccb342 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/coldctr/ColdDataCgCtrServiceTest.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.broker.coldctr; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.coldctr.AccAndTimeStamp; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ColdDataCgCtrServiceTest { + + @Mock + private BrokerController brokerController; + + @Mock + private BrokerConfig brokerConfig; + + private ColdDataCgCtrService coldDataCgCtrService; + + @Before + public void init() throws IllegalAccessException { + when(brokerController.getBrokerConfig()).thenReturn(brokerConfig); + coldDataCgCtrService = new ColdDataCgCtrService(brokerController); + FieldUtils.writeField(coldDataCgCtrService, "cgColdThresholdMapRuntime", createCgColdThresholdMapRuntime(), true); + FieldUtils.writeField(coldDataCgCtrService, "cgColdThresholdMapConfig", createCgColdThresholdMapConfig(), true); + } + + @Test + public void testGetColdDataFlowCtrInfo() { + String actual = coldDataCgCtrService.getColdDataFlowCtrInfo(); + assertTrue(actual.contains("\"globalAcc\":0")); + assertTrue(actual.contains("\"cgColdReadThreshold\":0")); + assertTrue(actual.contains("\"globalColdReadThreshold\":0")); + assertTrue(actual.contains("\"configTable\":{\"consumerGroup2\":2048}")); + assertTrue(actual.contains("\"runtimeTable\":{\"consumerGroup1\":{\"coldAcc\":1,\"createTimeMills\":1,\"lastColdReadTimeMills\":1}}")); + } + + private Map createCgColdThresholdMapRuntime() { + Map result = new ConcurrentHashMap<>(); + AccAndTimeStamp accAndTimeStamp = new AccAndTimeStamp(new AtomicLong(1L)); + accAndTimeStamp.setCreateTimeMills(1L); + accAndTimeStamp.setLastColdReadTimeMills(1L); + result.put("consumerGroup1", accAndTimeStamp); + return result; + } + + private ConcurrentHashMap createCgColdThresholdMapConfig() { + ConcurrentHashMap result = new ConcurrentHashMap<>(); + result.put("consumerGroup2", 2048L); + return result; + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/config/v2/ConsumerOffsetManagerV2Test.java b/broker/src/test/java/org/apache/rocketmq/broker/config/v2/ConsumerOffsetManagerV2Test.java new file mode 100644 index 0000000..132bd5c --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/config/v2/ConsumerOffsetManagerV2Test.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.config.v2; + +import java.io.File; +import java.io.IOException; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.offset.ConsumerOffsetManager; +import org.apache.rocketmq.common.BrokerConfig; +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.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class ConsumerOffsetManagerV2Test { + + private ConfigStorage configStorage; + + private ConsumerOffsetManagerV2 consumerOffsetManagerV2; + + @Mock + private BrokerController controller; + + private MessageStoreConfig messageStoreConfig; + + @Rule + public TemporaryFolder tf = new TemporaryFolder(); + + @After + public void cleanUp() { + if (null != configStorage) { + configStorage.shutdown(); + } + } + + @Before + public void setUp() throws IOException { + BrokerConfig brokerConfig = new BrokerConfig(); + Mockito.doReturn(brokerConfig).when(controller).getBrokerConfig(); + + File configStoreDir = tf.newFolder(); + messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setStorePathRootDir(configStoreDir.getAbsolutePath()); + configStorage = new ConfigStorage(messageStoreConfig); + configStorage.start(); + consumerOffsetManagerV2 = new ConsumerOffsetManagerV2(controller, configStorage); + } + + /** + * Verify consumer offset can survive restarts + */ + @Test + public void testCommitOffset_Standard() { + Assert.assertTrue(consumerOffsetManagerV2.load()); + + String clientHost = "localhost"; + String topic = "T1"; + String group = "G0"; + int queueId = 1; + long queueOffset = 100; + consumerOffsetManagerV2.commitOffset(clientHost, group, topic, queueId, queueOffset); + Assert.assertEquals(queueOffset, consumerOffsetManagerV2.queryOffset(group, topic, queueId)); + + configStorage.shutdown(); + consumerOffsetManagerV2.getOffsetTable().clear(); + Assert.assertEquals(-1L, consumerOffsetManagerV2.queryOffset(group, topic, queueId)); + + configStorage = new ConfigStorage(messageStoreConfig); + configStorage.start(); + consumerOffsetManagerV2 = new ConsumerOffsetManagerV2(controller, configStorage); + consumerOffsetManagerV2.load(); + Assert.assertEquals(queueOffset, consumerOffsetManagerV2.queryOffset(group, topic, queueId)); + } + + /** + * Verify commit offset can survive config store restart + */ + @Test + public void testCommitOffset_LMQ() { + Assert.assertTrue(consumerOffsetManagerV2.load()); + + String clientHost = "localhost"; + String topic = MixAll.LMQ_PREFIX + "T1"; + String group = "G0"; + int queueId = 1; + long queueOffset = 100; + consumerOffsetManagerV2.commitOffset(clientHost, group, topic, queueId, queueOffset); + Assert.assertEquals(queueOffset, consumerOffsetManagerV2.queryOffset(group, topic, queueId)); + + configStorage.shutdown(); + + configStorage = new ConfigStorage(messageStoreConfig); + configStorage.start(); + consumerOffsetManagerV2 = new ConsumerOffsetManagerV2(controller, configStorage); + consumerOffsetManagerV2.load(); + Assert.assertEquals(queueOffset, consumerOffsetManagerV2.queryOffset(group, topic, queueId)); + } + + + /** + * Verify commit offset can survive config store restart + */ + @Test + public void testCommitPullOffset_LMQ() { + Assert.assertTrue(consumerOffsetManagerV2.load()); + + String clientHost = "localhost"; + String topic = MixAll.LMQ_PREFIX + "T1"; + String group = "G0"; + int queueId = 1; + long queueOffset = 100; + consumerOffsetManagerV2.commitPullOffset(clientHost, group, topic, queueId, queueOffset); + Assert.assertEquals(queueOffset, consumerOffsetManagerV2.queryPullOffset(group, topic, queueId)); + + configStorage.shutdown(); + + configStorage = new ConfigStorage(messageStoreConfig); + configStorage.start(); + consumerOffsetManagerV2 = new ConsumerOffsetManagerV2(controller, configStorage); + consumerOffsetManagerV2.load(); + Assert.assertEquals(queueOffset, consumerOffsetManagerV2.queryPullOffset(group, topic, queueId)); + } + + /** + * Verify commit offset can survive config store restart + */ + @Test + public void testRemoveByTopicAtGroup() { + Assert.assertTrue(consumerOffsetManagerV2.load()); + + String clientHost = "localhost"; + String topic = MixAll.LMQ_PREFIX + "T1"; + String topic2 = MixAll.LMQ_PREFIX + "T2"; + String group = "G0"; + int queueId = 1; + long queueOffset = 100; + consumerOffsetManagerV2.commitOffset(clientHost, group, topic, queueId, queueOffset); + consumerOffsetManagerV2.commitOffset(clientHost, group, topic2, queueId, queueOffset); + Assert.assertEquals(queueOffset, consumerOffsetManagerV2.queryOffset(group, topic, queueId)); + Assert.assertEquals(queueOffset, consumerOffsetManagerV2.queryOffset(group, topic2, queueId)); + + consumerOffsetManagerV2.removeConsumerOffset(topic + ConsumerOffsetManager.TOPIC_GROUP_SEPARATOR + group); + Assert.assertEquals(-1L, consumerOffsetManagerV2.queryOffset(group, topic, queueId)); + Assert.assertEquals(queueOffset, consumerOffsetManagerV2.queryOffset(group, topic2, queueId)); + + configStorage.shutdown(); + + configStorage = new ConfigStorage(messageStoreConfig); + configStorage.start(); + consumerOffsetManagerV2 = new ConsumerOffsetManagerV2(controller, configStorage); + consumerOffsetManagerV2.load(); + Assert.assertEquals(-1L, consumerOffsetManagerV2.queryOffset(group, topic, queueId)); + Assert.assertEquals(queueOffset, consumerOffsetManagerV2.queryOffset(group, topic2, queueId)); + } + + /** + * Verify commit offset can survive config store restart + */ + @Test + public void testRemoveByGroup() { + Assert.assertTrue(consumerOffsetManagerV2.load()); + + String clientHost = "localhost"; + String topic = MixAll.LMQ_PREFIX + "T1"; + String topic2 = MixAll.LMQ_PREFIX + "T2"; + String group = "G0"; + int queueId = 1; + long queueOffset = 100; + consumerOffsetManagerV2.commitOffset(clientHost, group, topic, queueId, queueOffset); + consumerOffsetManagerV2.commitOffset(clientHost, group, topic2, queueId, queueOffset); + Assert.assertEquals(queueOffset, consumerOffsetManagerV2.queryOffset(group, topic, queueId)); + consumerOffsetManagerV2.removeOffset(group); + Assert.assertEquals(-1L, consumerOffsetManagerV2.queryOffset(group, topic, queueId)); + Assert.assertEquals(-1L, consumerOffsetManagerV2.queryOffset(group, topic2, queueId)); + + configStorage.shutdown(); + + configStorage = new ConfigStorage(messageStoreConfig); + configStorage.start(); + consumerOffsetManagerV2 = new ConsumerOffsetManagerV2(controller, configStorage); + consumerOffsetManagerV2.load(); + Assert.assertEquals(-1L, consumerOffsetManagerV2.queryOffset(group, topic, queueId)); + Assert.assertEquals(-1L, consumerOffsetManagerV2.queryOffset(group, topic2, queueId)); + } + +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/config/v2/SubscriptionGroupManagerV2Test.java b/broker/src/test/java/org/apache/rocketmq/broker/config/v2/SubscriptionGroupManagerV2Test.java new file mode 100644 index 0000000..4ff8a81 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/config/v2/SubscriptionGroupManagerV2Test.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.config.v2; + +import java.io.File; +import java.io.IOException; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.remoting.protocol.subscription.GroupRetryPolicy; +import org.apache.rocketmq.remoting.protocol.subscription.GroupRetryPolicyType; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class SubscriptionGroupManagerV2Test { + + private MessageStoreConfig messageStoreConfig; + + private ConfigStorage configStorage; + + private SubscriptionGroupManagerV2 subscriptionGroupManagerV2; + + @Mock + private BrokerController controller; + + @Mock + private MessageStore messageStore; + + @Rule + public TemporaryFolder tf = new TemporaryFolder(); + + @After + public void cleanUp() { + if (null != configStorage) { + configStorage.shutdown(); + } + } + + @Before + public void setUp() throws IOException { + BrokerConfig brokerConfig = new BrokerConfig(); + brokerConfig.setAutoCreateSubscriptionGroup(false); + Mockito.doReturn(brokerConfig).when(controller).getBrokerConfig(); + + Mockito.doReturn(messageStore).when(controller).getMessageStore(); + Mockito.doReturn(1L).when(messageStore).getStateMachineVersion(); + + File configStoreDir = tf.newFolder(); + messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setStorePathRootDir(configStoreDir.getAbsolutePath()); + configStorage = new ConfigStorage(messageStoreConfig); + configStorage.start(); + subscriptionGroupManagerV2 = new SubscriptionGroupManagerV2(controller, configStorage); + } + + + @Test + public void testUpdateSubscriptionGroupConfig() { + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setGroupName("G1"); + subscriptionGroupConfig.setConsumeEnable(true); + subscriptionGroupConfig.setRetryMaxTimes(16); + subscriptionGroupConfig.setGroupSysFlag(1); + GroupRetryPolicy retryPolicy = new GroupRetryPolicy(); + retryPolicy.setType(GroupRetryPolicyType.EXPONENTIAL); + subscriptionGroupConfig.setGroupRetryPolicy(retryPolicy); + subscriptionGroupConfig.setBrokerId(1); + subscriptionGroupConfig.setConsumeBroadcastEnable(true); + subscriptionGroupConfig.setConsumeMessageOrderly(true); + subscriptionGroupConfig.setConsumeTimeoutMinute(30); + subscriptionGroupConfig.setConsumeFromMinEnable(true); + subscriptionGroupConfig.setWhichBrokerWhenConsumeSlowly(1); + subscriptionGroupConfig.setNotifyConsumerIdsChangedEnable(true); + subscriptionGroupManagerV2.updateSubscriptionGroupConfig(subscriptionGroupConfig); + + SubscriptionGroupConfig found = subscriptionGroupManagerV2.findSubscriptionGroupConfig(subscriptionGroupConfig.getGroupName()); + Assert.assertEquals(subscriptionGroupConfig, found); + + subscriptionGroupManagerV2.getSubscriptionGroupTable().clear(); + configStorage.shutdown(); + + configStorage = new ConfigStorage(messageStoreConfig); + configStorage.start(); + subscriptionGroupManagerV2 = new SubscriptionGroupManagerV2(controller, configStorage); + subscriptionGroupManagerV2.load(); + found = subscriptionGroupManagerV2.findSubscriptionGroupConfig(subscriptionGroupConfig.getGroupName()); + Assert.assertEquals(subscriptionGroupConfig, found); + } + + + @Test + public void testDeleteSubscriptionGroupConfig() { + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setGroupName("G1"); + subscriptionGroupConfig.setConsumeEnable(true); + subscriptionGroupConfig.setRetryMaxTimes(16); + subscriptionGroupConfig.setGroupSysFlag(1); + GroupRetryPolicy retryPolicy = new GroupRetryPolicy(); + retryPolicy.setType(GroupRetryPolicyType.EXPONENTIAL); + subscriptionGroupConfig.setGroupRetryPolicy(retryPolicy); + subscriptionGroupConfig.setBrokerId(1); + subscriptionGroupConfig.setConsumeBroadcastEnable(true); + subscriptionGroupConfig.setConsumeMessageOrderly(true); + subscriptionGroupConfig.setConsumeTimeoutMinute(30); + subscriptionGroupConfig.setConsumeFromMinEnable(true); + subscriptionGroupConfig.setWhichBrokerWhenConsumeSlowly(1); + subscriptionGroupConfig.setNotifyConsumerIdsChangedEnable(true); + subscriptionGroupManagerV2.updateSubscriptionGroupConfig(subscriptionGroupConfig); + + SubscriptionGroupConfig found = subscriptionGroupManagerV2.findSubscriptionGroupConfig(subscriptionGroupConfig.getGroupName()); + Assert.assertEquals(subscriptionGroupConfig, found); + subscriptionGroupManagerV2.removeSubscriptionGroupConfig(subscriptionGroupConfig.getGroupName()); + + found = subscriptionGroupManagerV2.findSubscriptionGroupConfig(subscriptionGroupConfig.getGroupName()); + Assert.assertNull(found); + + configStorage.shutdown(); + + configStorage = new ConfigStorage(messageStoreConfig); + configStorage.start(); + + subscriptionGroupManagerV2 = new SubscriptionGroupManagerV2(controller, configStorage); + subscriptionGroupManagerV2.load(); + found = subscriptionGroupManagerV2.findSubscriptionGroupConfig(subscriptionGroupConfig.getGroupName()); + Assert.assertNull(found); + } + +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/config/v2/TopicConfigManagerV2Test.java b/broker/src/test/java/org/apache/rocketmq/broker/config/v2/TopicConfigManagerV2Test.java new file mode 100644 index 0000000..731a1f5 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/config/v2/TopicConfigManagerV2Test.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.config.v2; + +import java.io.File; +import java.io.IOException; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(value = MockitoJUnitRunner.class) +public class TopicConfigManagerV2Test { + + private MessageStoreConfig messageStoreConfig; + + private ConfigStorage configStorage; + + @Mock + private BrokerController controller; + + @Mock + private MessageStore messageStore; + + @Rule + public TemporaryFolder tf = new TemporaryFolder(); + + @After + public void cleanUp() { + if (null != configStorage) { + configStorage.shutdown(); + } + } + + @Before + public void setUp() throws IOException { + BrokerConfig brokerConfig = new BrokerConfig(); + Mockito.doReturn(brokerConfig).when(controller).getBrokerConfig(); + + messageStoreConfig = new MessageStoreConfig(); + Mockito.doReturn(messageStoreConfig).when(controller).getMessageStoreConfig(); + Mockito.doReturn(messageStore).when(controller).getMessageStore(); + + File configStoreDir = tf.newFolder(); + messageStoreConfig.setStorePathRootDir(configStoreDir.getAbsolutePath()); + + configStorage = new ConfigStorage(messageStoreConfig); + configStorage.start(); + } + + @Test + public void testUpdateTopicConfig() { + TopicConfigManagerV2 topicConfigManagerV2 = new TopicConfigManagerV2(controller, configStorage); + topicConfigManagerV2.load(); + + TopicConfig topicConfig = new TopicConfig(); + String topicName = "T1"; + topicConfig.setTopicName(topicName); + topicConfig.setPerm(6); + topicConfig.setReadQueueNums(8); + topicConfig.setWriteQueueNums(4); + topicConfig.setOrder(true); + topicConfig.setTopicSysFlag(4); + topicConfigManagerV2.updateTopicConfig(topicConfig); + + Assert.assertTrue(configStorage.shutdown()); + + topicConfigManagerV2.getTopicConfigTable().clear(); + + configStorage = new ConfigStorage(messageStoreConfig); + Assert.assertTrue(configStorage.start()); + topicConfigManagerV2 = new TopicConfigManagerV2(controller, configStorage); + Assert.assertTrue(topicConfigManagerV2.load()); + + TopicConfig loaded = topicConfigManagerV2.selectTopicConfig(topicName); + Assert.assertNotNull(loaded); + Assert.assertEquals(topicName, loaded.getTopicName()); + Assert.assertEquals(6, loaded.getPerm()); + Assert.assertEquals(8, loaded.getReadQueueNums()); + Assert.assertEquals(4, loaded.getWriteQueueNums()); + Assert.assertTrue(loaded.isOrder()); + Assert.assertEquals(4, loaded.getTopicSysFlag()); + + Assert.assertTrue(topicConfigManagerV2.containsTopic(topicName)); + } + + @Test + public void testRemoveTopicConfig() { + TopicConfig topicConfig = new TopicConfig(); + String topicName = "T1"; + topicConfig.setTopicName(topicName); + topicConfig.setPerm(6); + topicConfig.setReadQueueNums(8); + topicConfig.setWriteQueueNums(4); + topicConfig.setOrder(true); + topicConfig.setTopicSysFlag(4); + TopicConfigManagerV2 topicConfigManagerV2 = new TopicConfigManagerV2(controller, configStorage); + topicConfigManagerV2.updateTopicConfig(topicConfig); + topicConfigManagerV2.removeTopicConfig(topicName); + Assert.assertFalse(topicConfigManagerV2.containsTopic(topicName)); + Assert.assertTrue(configStorage.shutdown()); + + configStorage = new ConfigStorage(messageStoreConfig); + Assert.assertTrue(configStorage.start()); + topicConfigManagerV2 = new TopicConfigManagerV2(controller, configStorage); + Assert.assertTrue(topicConfigManagerV2.load()); + Assert.assertFalse(topicConfigManagerV2.containsTopic(topicName)); + } +} 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 0000000..39ec0d8 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/controller/ReplicasManagerRegisterTest.java @@ -0,0 +1,351 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.commons.lang3.reflect.FieldUtils; +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.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.File; +import java.time.Duration; +import java.util.Collections; +import java.util.HashSet; +import java.util.UUID; + +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.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<>(Collections.singletonList(1L)); + + @Mock + private BrokerMetadata brokerMetadata; + + @Mock + private TempBrokerMetadata tempBrokerMetadata; + + 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 + ); + assertEquals(ReplicasManager.RegisterState.REGISTERED, replicasManager0.getRegisterState()); + assertEquals(1L, replicasManager0.getBrokerControllerId().longValue()); + checkMetadataFile(replicasManager0.getBrokerMetadata(), 1L); + assertFalse(replicasManager0.getTempBrokerMetadata().isLoaded()); + 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 + ); + assertEquals(ReplicasManager.RegisterState.REGISTERED, replicasManager0.getRegisterState()); + assertEquals(1L, replicasManager0.getBrokerControllerId().longValue()); + checkMetadataFile(replicasManager0.getBrokerMetadata(), 1L); + assertFalse(replicasManager0.getTempBrokerMetadata().isLoaded()); + 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(); + 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(); + 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(); + + assertEquals(ReplicasManager.State.FIRST_TIME_SYNC_CONTROLLER_METADATA_DONE, replicasManager.getState()); + assertEquals(ReplicasManager.RegisterState.INITIAL, replicasManager.getRegisterState()); + assertFalse(replicasManager.getTempBrokerMetadata().fileExists()); + assertFalse(replicasManager.getBrokerMetadata().fileExists()); + assertNull(replicasManager.getBrokerControllerId()); + replicasManager.shutdown(); + } + + @Test + public void testRegisterFailedAtCreateTempFile() throws Exception { + ReplicasManager spyReplicasManager = new ReplicasManager(mockedBrokerController); + when(mockedBrokerOuterAPI.getNextBrokerId(any(), any(), any())).thenReturn(new GetNextBrokerIdResponseHeader(CLUSTER_NAME, BROKER_NAME, 1L)); + FieldUtils.writeDeclaredField(spyReplicasManager, "tempBrokerMetadata", tempBrokerMetadata, true); + doThrow(new RuntimeException("Test exception")).when(tempBrokerMetadata).updateAndPersist(any(), any(), anyLong(), any()); + + spyReplicasManager.start(); + + assertEquals(ReplicasManager.State.FIRST_TIME_SYNC_CONTROLLER_METADATA_DONE, spyReplicasManager.getState()); + assertEquals(ReplicasManager.RegisterState.INITIAL, spyReplicasManager.getRegisterState()); + assertFalse(spyReplicasManager.getTempBrokerMetadata().fileExists()); + assertFalse(spyReplicasManager.getBrokerMetadata().fileExists()); + 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()); + + replicasManager.start(); + + assertEquals(ReplicasManager.State.FIRST_TIME_SYNC_CONTROLLER_METADATA_DONE, replicasManager.getState()); + assertNotEquals(ReplicasManager.RegisterState.CREATE_METADATA_FILE_DONE, replicasManager.getRegisterState()); + assertNotEquals(ReplicasManager.RegisterState.REGISTERED, replicasManager.getRegisterState()); + + replicasManager.shutdown(); + + assertFalse(replicasManager.getBrokerMetadata().fileExists()); + assertNull(replicasManager.getBrokerControllerId()); + } + + @Test + public void testRegisterFailedAtCreateMetadataFileAndDeleteTemp() throws Exception { + ReplicasManager spyReplicasManager = 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)); + + FieldUtils.writeDeclaredField(spyReplicasManager, "brokerMetadata", brokerMetadata, true); + doThrow(new RuntimeException("Test exception")).when(brokerMetadata).updateAndPersist(any(), any(), anyLong()); + + spyReplicasManager.start(); + + assertEquals(ReplicasManager.State.FIRST_TIME_SYNC_CONTROLLER_METADATA_DONE, spyReplicasManager.getState()); + assertEquals(ReplicasManager.RegisterState.CREATE_TEMP_METADATA_FILE_DONE, spyReplicasManager.getRegisterState()); + TempBrokerMetadata tempBrokerMetadata = spyReplicasManager.getTempBrokerMetadata(); + assertTrue(tempBrokerMetadata.fileExists()); + assertTrue(tempBrokerMetadata.isLoaded()); + assertFalse(spyReplicasManager.getBrokerMetadata().fileExists()); + 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); + + replicasManagerNew.start(); + + assertEquals(ReplicasManager.State.RUNNING, replicasManagerNew.getState()); + assertEquals(ReplicasManager.RegisterState.REGISTERED, replicasManagerNew.getRegisterState()); + // tempMetadata has been cleared + assertFalse(replicasManagerNew.getTempBrokerMetadata().fileExists()); + assertFalse(replicasManagerNew.getTempBrokerMetadata().isLoaded()); + // metadata has been persisted + assertTrue(replicasManagerNew.getBrokerMetadata().fileExists()); + assertTrue(replicasManagerNew.getBrokerMetadata().isLoaded()); + assertEquals(1L, replicasManagerNew.getBrokerMetadata().getBrokerId().longValue()); + 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(); + + assertEquals(ReplicasManager.State.FIRST_TIME_SYNC_CONTROLLER_METADATA_DONE, replicasManager.getState()); + assertEquals(ReplicasManager.RegisterState.CREATE_METADATA_FILE_DONE, replicasManager.getRegisterState()); + TempBrokerMetadata tempBrokerMetadata = replicasManager.getTempBrokerMetadata(); + // temp metadata has been cleared + assertFalse(tempBrokerMetadata.fileExists()); + assertFalse(tempBrokerMetadata.isLoaded()); + // metadata has been persisted + assertTrue(replicasManager.getBrokerMetadata().fileExists()); + assertTrue(replicasManager.getBrokerMetadata().isLoaded()); + assertEquals(1L, replicasManager.getBrokerMetadata().getBrokerId().longValue()); + 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); + when(mockedBrokerOuterAPI.registerBrokerToController(any(), any(), anyLong(), any(), any())).thenReturn(new Pair<>(new RegisterBrokerToControllerResponseHeader(), syncStateSet)); + replicasManagerNew.start(); + + assertEquals(ReplicasManager.State.RUNNING, replicasManagerNew.getState()); + assertEquals(ReplicasManager.RegisterState.REGISTERED, replicasManagerNew.getRegisterState()); + // tempMetadata has been cleared + assertFalse(replicasManagerNew.getTempBrokerMetadata().fileExists()); + assertFalse(replicasManagerNew.getTempBrokerMetadata().isLoaded()); + // metadata has been persisted + assertTrue(replicasManagerNew.getBrokerMetadata().fileExists()); + assertTrue(replicasManagerNew.getBrokerMetadata().isLoaded()); + assertEquals(1L, replicasManagerNew.getBrokerMetadata().getBrokerId().longValue()); + assertEquals(1L, replicasManagerNew.getBrokerControllerId().longValue()); + replicasManagerNew.shutdown(); + } + + + private void checkMetadataFile(BrokerMetadata brokerMetadata0 ,Long brokerId) throws Exception { + assertEquals(brokerId, brokerMetadata0.getBrokerId()); + assertTrue(brokerMetadata0.fileExists()); + BrokerMetadata brokerMetadata = new BrokerMetadata(brokerMetadata0.getFilePath()); + brokerMetadata.readFromFile(); + 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 0000000..9f17f2b --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/controller/ReplicasManagerTest.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.broker.controller; + +import com.google.common.collect.Lists; +import java.io.File; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +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.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.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.ArgumentMatchers.anyString; +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(); + } + + @Test + public void testUpdateControllerAddr() throws Exception { + final String controllerAddr = "192.168.1.1"; + brokerConfig.setFetchControllerAddrByDnsLookup(true); + when(brokerOuterAPI.dnsLookupAddressByDomain(anyString())).thenReturn(Lists.newArrayList(controllerAddr)); + Method method = ReplicasManager.class.getDeclaredMethod("updateControllerAddr"); + method.setAccessible(true); + method.invoke(replicasManager); + + List addresses = replicasManager.getControllerAddresses(); + Assertions.assertThat(addresses).contains(controllerAddr); + + // Simulating dns resolution exceptions + when(brokerOuterAPI.dnsLookupAddressByDomain(anyString())).thenReturn(new ArrayList<>()); + + method.invoke(replicasManager); + addresses = replicasManager.getControllerAddresses(); + Assertions.assertThat(addresses).contains(controllerAddr); + + } + +} 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 0000000..27fc37d --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/failover/EscapeBridgeTest.java @@ -0,0 +1,432 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Triple; +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.impl.producer.TopicPublishInfo; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.common.BrokerConfig; +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.remoting.exception.RemotingException; +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.SelectMappedBufferResult; +import org.apache.rocketmq.store.logfile.DefaultMappedFile; +import org.apache.rocketmq.store.logfile.MappedFile; +import org.apache.rocketmq.tieredstore.TieredMessageStore; +import org.assertj.core.api.Assertions; +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 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.when; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.verify; + +@RunWith(MockitoJUnitRunner.class) +public class EscapeBridgeTest { + + private EscapeBridge escapeBridge; + + @Mock + private BrokerController brokerController; + + @Mock + private MessageExtBrokerInner messageExtBrokerInner; + + private BrokerConfig brokerConfig; + + @Mock + private DefaultMessageStore defaultMessageStore; + + @Mock + private TieredMessageStore tieredMessageStore; + + private GetMessageResult getMessageResult; + + @Mock + private DefaultMQProducer defaultMQProducer; + + @Mock + private TopicRouteInfoManager topicRouteInfoManager; + + @Mock + private BrokerOuterAPI brokerOuterAPI; + + 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)); + + when(brokerController.getTopicRouteInfoManager()).thenReturn(topicRouteInfoManager); + when(topicRouteInfoManager.findBrokerAddressInSubscribe(anyString(), anyLong(), anyBoolean())).thenReturn(""); + + when(brokerController.getBrokerOuterAPI()).thenReturn(brokerOuterAPI); + + 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 getMessageAsyncTest_localStore_getMessageAsync_null() { + when(brokerController.getMessageStoreByBrokerName(any())).thenReturn(defaultMessageStore); + when(defaultMessageStore.getMessageAsync(anyString(), anyString(), anyInt(), anyLong(), anyInt(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + Triple rst = escapeBridge.getMessageAsync(TEST_TOPIC, 0, DEFAULT_QUEUE_ID, BROKER_NAME, false).join(); + Assert.assertNull(rst.getLeft()); + Assert.assertEquals("getMessageResult is null", rst.getMiddle()); + Assert.assertFalse(rst.getRight()); // no retry + } + + @Test + public void getMessageAsyncTest_localStore_decodeNothing_DefaultMessageStore() throws Exception { + when(brokerController.getMessageStoreByBrokerName(any())).thenReturn(defaultMessageStore); + for (GetMessageStatus status : GetMessageStatus.values()) { + GetMessageResult getMessageResult = mockGetMessageResult(0, TEST_TOPIC, null); + getMessageResult.setStatus(status); + when(defaultMessageStore.getMessageAsync(anyString(), anyString(), anyInt(), anyLong(), anyInt(), any())) + .thenReturn(CompletableFuture.completedFuture(getMessageResult)); + Triple rst = escapeBridge.getMessageAsync(TEST_TOPIC, 0, DEFAULT_QUEUE_ID, BROKER_NAME, false).join(); + Assert.assertNull(rst.getLeft()); + Assert.assertEquals("Can not get msg", rst.getMiddle()); + Assert.assertFalse(rst.getRight()); // DefaultMessageStore, no retry + } + } + + @Test + public void getMessageAsyncTest_localStore_decodeNothing_TieredMessageStore() throws Exception { + when(brokerController.getMessageStoreByBrokerName(any())).thenReturn(tieredMessageStore); + for (GetMessageStatus status : GetMessageStatus.values()) { + GetMessageResult getMessageResult = new GetMessageResult(); + getMessageResult.setStatus(status); + when(tieredMessageStore.getMessageAsync(anyString(), anyString(), anyInt(), anyLong(), anyInt(), any())) + .thenReturn(CompletableFuture.completedFuture(getMessageResult)); + Triple rst = escapeBridge.getMessageAsync(TEST_TOPIC, 0, DEFAULT_QUEUE_ID, BROKER_NAME, false).join(); + Assert.assertNull(rst.getLeft()); + Assert.assertEquals("Can not get msg", rst.getMiddle()); + if (GetMessageStatus.OFFSET_FOUND_NULL.equals(status)) { + Assert.assertTrue(rst.getRight()); // TieredMessageStore returns OFFSET_FOUND_NULL, need retry + } else { + Assert.assertFalse(rst.getRight()); // other status, like DefaultMessageStore, no retry + } + } + } + + @Test + public void getMessageAsyncTest_localStore_message_found() throws Exception { + when(brokerController.getMessageStoreByBrokerName(any())).thenReturn(defaultMessageStore); + when(defaultMessageStore.getMessageAsync(anyString(), anyString(), anyInt(), anyLong(), anyInt(), any())) + .thenReturn(CompletableFuture.completedFuture(mockGetMessageResult(2, TEST_TOPIC, "HW".getBytes()))); + Triple rst = escapeBridge.getMessageAsync(TEST_TOPIC, 0, DEFAULT_QUEUE_ID, BROKER_NAME, false).join(); + Assert.assertNotNull(rst.getLeft()); + Assert.assertEquals(0, rst.getLeft().getQueueOffset()); + Assert.assertTrue(Arrays.equals("HW".getBytes(), rst.getLeft().getBody())); + Assert.assertFalse(rst.getRight()); + } + + @Test + public void getMessageAsyncTest_remoteStore_addressNotFound() throws Exception { + when(brokerController.getMessageStoreByBrokerName(any())).thenReturn(null); + + // just test address not found, since we have complete tests of getMessageFromRemoteAsync() + when(topicRouteInfoManager.findBrokerAddressInSubscribe(anyString(), anyLong(), anyBoolean())).thenReturn(null); + Triple rst = escapeBridge.getMessageAsync(TEST_TOPIC, 0, DEFAULT_QUEUE_ID, BROKER_NAME, false).join(); + Assert.assertNull(rst.getLeft()); + Assert.assertEquals("brokerAddress not found", rst.getMiddle()); + Assert.assertTrue(rst.getRight()); // need retry + } + + @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 getMessageFromRemoteAsyncTest_exception_caught() throws Exception { + when(brokerOuterAPI.pullMessageFromSpecificBrokerAsync(anyString(), anyString(), anyString(), anyString(), anyInt(), anyLong(), anyInt(), anyLong())) + .thenThrow(new RemotingException("mock remoting exception")); + Triple rst = escapeBridge.getMessageFromRemoteAsync(TEST_TOPIC, 1, DEFAULT_QUEUE_ID, BROKER_NAME).join(); + Assert.assertNull(rst.getLeft()); + Assert.assertEquals("Get message from remote failed", rst.getMiddle()); + Assert.assertTrue(rst.getRight()); // need retry + } + + @Test + public void getMessageFromRemoteAsyncTest_brokerAddressNotFound() throws Exception { + when(topicRouteInfoManager.findBrokerAddressInSubscribe(anyString(), anyLong(), anyBoolean())).thenReturn(null); + Triple rst = escapeBridge.getMessageFromRemoteAsync(TEST_TOPIC, 1, DEFAULT_QUEUE_ID, BROKER_NAME).join(); + Assert.assertNull(rst.getLeft()); + Assert.assertEquals("brokerAddress not found", rst.getMiddle()); + Assert.assertTrue(rst.getRight()); // need retry + } + + @Test + public void getMessageFromRemoteAsyncTest_message_found() throws Exception { + PullResult pullResult = new PullResult(PullStatus.FOUND, 1, 1, 1, Arrays.asList(new MessageExt())); + when(brokerOuterAPI.pullMessageFromSpecificBrokerAsync(anyString(), anyString(), anyString(), anyString(), anyInt(), anyLong(), anyInt(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(Triple.of(pullResult, "", false))); // right value is ignored + Triple rst = escapeBridge.getMessageFromRemoteAsync(TEST_TOPIC, 1, DEFAULT_QUEUE_ID, BROKER_NAME).join(); + Assert.assertNotNull(rst.getLeft()); + Assert.assertTrue(StringUtils.isEmpty(rst.getMiddle())); + Assert.assertFalse(rst.getRight()); // no retry + } + + @Test + public void getMessageFromRemoteAsyncTest_message_notFound() throws Exception { + PullResult pullResult = new PullResult(PullStatus.NO_MATCHED_MSG, 1, 1, 1, null); + when(brokerOuterAPI.pullMessageFromSpecificBrokerAsync(anyString(), anyString(), anyString(), anyString(), anyInt(), anyLong(), anyInt(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(Triple.of(pullResult, "no msg", false))); + Triple rst = escapeBridge.getMessageFromRemoteAsync(TEST_TOPIC, 1, DEFAULT_QUEUE_ID, BROKER_NAME).join(); + Assert.assertNull(rst.getLeft()); + Assert.assertEquals("no msg", rst.getMiddle()); + Assert.assertFalse(rst.getRight()); // no retry + + when(brokerOuterAPI.pullMessageFromSpecificBrokerAsync(anyString(), anyString(), anyString(), anyString(), anyInt(), anyLong(), anyInt(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(Triple.of(null, "other resp code", true))); + rst = escapeBridge.getMessageFromRemoteAsync(TEST_TOPIC, 1, DEFAULT_QUEUE_ID, BROKER_NAME).join(); + Assert.assertNull(rst.getLeft()); + Assert.assertEquals("other resp code", rst.getMiddle()); + Assert.assertTrue(rst.getRight()); // need retry + } + + @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(); + } + + @Test + public void decodeMsgListTest_messageNotNull() throws Exception { + MessageExt msg = new MessageExt(); + msg.setBody("HW".getBytes()); + msg.setTopic("topic"); + msg.setBornHost(new InetSocketAddress("127.0.0.1", 9000)); + msg.setStoreHost(new InetSocketAddress("127.0.0.1", 9000)); + ByteBuffer byteBuffer = ByteBuffer.wrap(MessageDecoder.encode(msg, false)); + SelectMappedBufferResult result = new SelectMappedBufferResult(0, byteBuffer, 10, new DefaultMappedFile()); + + + getMessageResult.addMessage(result); + getMessageResult.getMessageQueueOffset().add(0L); + List list = escapeBridge.decodeMsgList(getMessageResult, false); // skip deCompressBody test + Assert.assertEquals(1, list.size()); + Assert.assertTrue(Arrays.equals(msg.getBody(), list.get(0).getBody())); + } + + @Test + public void testPutMessageToRemoteBroker_noSpecificBrokerName_hasRemoteBroker() throws Exception { + MessageExtBrokerInner message = new MessageExtBrokerInner(); + message.setTopic(TEST_TOPIC); + String anotherBrokerName = "broker_b"; + TopicPublishInfo publishInfo = mockTopicPublishInfo(BROKER_NAME, anotherBrokerName); + when(topicRouteInfoManager.tryToFindTopicPublishInfo(anyString())).thenReturn(publishInfo); + when(topicRouteInfoManager.findBrokerAddressInPublish(anotherBrokerName)).thenReturn("127.0.0.1"); + escapeBridge.putMessageToRemoteBroker(message, null); + verify(brokerOuterAPI).sendMessageToSpecificBroker(eq("127.0.0.1"), eq(anotherBrokerName), any(MessageExtBrokerInner.class), anyString(), anyLong()); + } + + @Test + public void testPutMessageToRemoteBroker_noSpecificBrokerName_noRemoteBroker() throws Exception { + MessageExtBrokerInner message = new MessageExtBrokerInner(); + message.setTopic(TEST_TOPIC); + TopicPublishInfo publishInfo = mockTopicPublishInfo(BROKER_NAME); + when(topicRouteInfoManager.tryToFindTopicPublishInfo(anyString())).thenReturn(publishInfo); + escapeBridge.putMessageToRemoteBroker(message, null); + verify(topicRouteInfoManager, times(0)).findBrokerAddressInPublish(anyString()); + } + + @Test + public void testPutMessageToRemoteBroker_specificBrokerName_equals() throws Exception { + escapeBridge.putMessageToRemoteBroker(new MessageExtBrokerInner(), BROKER_NAME); + verify(topicRouteInfoManager, times(0)).tryToFindTopicPublishInfo(anyString()); + } + + @Test + public void testPutMessageToRemoteBroker_specificBrokerName_addressNotFound() throws Exception { + MessageExtBrokerInner message = new MessageExtBrokerInner(); + message.setTopic(TEST_TOPIC); + TopicPublishInfo publishInfo = mockTopicPublishInfo(BROKER_NAME); + when(topicRouteInfoManager.tryToFindTopicPublishInfo(anyString())).thenReturn(publishInfo); + escapeBridge.putMessageToRemoteBroker(message, "whatever"); + verify(topicRouteInfoManager).findBrokerAddressInPublish(eq("whatever")); + verify(brokerOuterAPI, times(0)).sendMessageToSpecificBroker(anyString(), anyString(), any(MessageExtBrokerInner.class), anyString(), anyLong()); + } + + @Test + public void testPutMessageToRemoteBroker_specificBrokerName_addressFound() throws Exception { + MessageExtBrokerInner message = new MessageExtBrokerInner(); + message.setTopic(TEST_TOPIC); + String anotherBrokerName = "broker_b"; + TopicPublishInfo publishInfo = mockTopicPublishInfo(BROKER_NAME, anotherBrokerName); + when(topicRouteInfoManager.tryToFindTopicPublishInfo(anyString())).thenReturn(publishInfo); + when(topicRouteInfoManager.findBrokerAddressInPublish(anotherBrokerName)).thenReturn("127.0.0.1"); + escapeBridge.putMessageToRemoteBroker(message, anotherBrokerName); + verify(brokerOuterAPI).sendMessageToSpecificBroker(eq("127.0.0.1"), eq(anotherBrokerName), any(MessageExtBrokerInner.class), anyString(), anyLong()); + } + + private GetMessageResult mockGetMessageResult(int count, String topic, byte[] body) throws Exception { + GetMessageResult result = new GetMessageResult(); + for (int i = 0; i < count; i++) { + MessageExt msg = new MessageExt(); + msg.setBody(body); + msg.setTopic(topic); + msg.setBornHost(new InetSocketAddress("127.0.0.1", 9000)); + msg.setStoreHost(new InetSocketAddress("127.0.0.1", 9000)); + ByteBuffer byteBuffer = ByteBuffer.wrap(MessageDecoder.encode(msg, false)); + SelectMappedBufferResult bufferResult = new SelectMappedBufferResult(0, byteBuffer, body.length, new DefaultMappedFile()); + + result.addMessage(bufferResult); + result.getMessageQueueOffset().add(i + 0L); + } + return result; + } + + private TopicPublishInfo mockTopicPublishInfo(String... brokerNames) { + TopicPublishInfo topicPublishInfo = new TopicPublishInfo(); + for (String brokerName : brokerNames) { + topicPublishInfo.getMessageQueueList().add(new MessageQueue(TEST_TOPIC, brokerName, 0)); + } + return topicPublishInfo; + } + +} 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 new file mode 100644 index 0000000..af1d06e --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/filter/CommitLogDispatcherCalcBitMapTest.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.broker.filter; + +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.filter.ExpressionType; +import org.apache.rocketmq.filter.util.BitsArray; +import org.apache.rocketmq.store.DispatchRequest; +import org.junit.Test; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CommitLogDispatcherCalcBitMapTest { + + @Test + public void testDispatch_filterDataIllegal() { + BrokerConfig brokerConfig = new BrokerConfig(); + brokerConfig.setEnableCalcFilterBitMap(true); + + ConsumerFilterManager filterManager = new ConsumerFilterManager(); + + filterManager.register("topic0", "CID_0", "a is not null and a >= 5", + ExpressionType.SQL92, System.currentTimeMillis()); + + filterManager.register("topic0", "CID_1", "a is not null and a >= 15", + ExpressionType.SQL92, System.currentTimeMillis()); + + ConsumerFilterData nullExpression = filterManager.get("topic0", "CID_0"); + nullExpression.setExpression(null); + nullExpression.setCompiledExpression(null); + ConsumerFilterData nullBloomData = filterManager.get("topic0", "CID_1"); + nullBloomData.setBloomFilterData(null); + + CommitLogDispatcherCalcBitMap calcBitMap = new CommitLogDispatcherCalcBitMap(brokerConfig, + filterManager); + + for (int i = 0; i < 1; i++) { + Map properties = new HashMap<>(4); + properties.put("a", String.valueOf(i * 10 + 5)); + + String topic = "topic" + i; + + DispatchRequest dispatchRequest = new DispatchRequest( + topic, + 0, + i * 100 + 123, + 100, + (long) ("tags" + i).hashCode(), + System.currentTimeMillis(), + i, + null, + UUID.randomUUID().toString(), + 0, + 0, + properties + ); + + calcBitMap.dispatch(dispatchRequest); + + assertThat(dispatchRequest.getBitMap()).isNotNull(); + + BitsArray bitsArray = BitsArray.create(dispatchRequest.getBitMap(), + filterManager.getBloomFilter().getM()); + + for (int j = 0; j < bitsArray.bitLength(); j++) { + assertThat(bitsArray.getBit(j)).isFalse(); + } + } + } + + @Test + public void testDispatch_blankFilterData() { + BrokerConfig brokerConfig = new BrokerConfig(); + brokerConfig.setEnableCalcFilterBitMap(true); + + ConsumerFilterManager filterManager = new ConsumerFilterManager(); + + CommitLogDispatcherCalcBitMap calcBitMap = new CommitLogDispatcherCalcBitMap(brokerConfig, + filterManager); + + for (int i = 0; i < 10; i++) { + Map properties = new HashMap<>(4); + properties.put("a", String.valueOf(i * 10 + 5)); + + String topic = "topic" + i; + + DispatchRequest dispatchRequest = new DispatchRequest( + topic, + 0, + i * 100 + 123, + 100, + (long) ("tags" + i).hashCode(), + System.currentTimeMillis(), + i, + null, + UUID.randomUUID().toString(), + 0, + 0, + properties + ); + + calcBitMap.dispatch(dispatchRequest); + + assertThat(dispatchRequest.getBitMap()).isNull(); + } + } + + @Test + public void testDispatch() { + BrokerConfig brokerConfig = new BrokerConfig(); + brokerConfig.setEnableCalcFilterBitMap(true); + + ConsumerFilterManager filterManager = ConsumerFilterManagerTest.gen(10, 10); + + CommitLogDispatcherCalcBitMap calcBitMap = new CommitLogDispatcherCalcBitMap(brokerConfig, + filterManager); + + for (int i = 0; i < 10; i++) { + Map properties = new HashMap<>(4); + properties.put("a", String.valueOf(i * 10 + 5)); + + String topic = "topic" + i; + + DispatchRequest dispatchRequest = new DispatchRequest( + topic, + 0, + i * 100 + 123, + 100, + (long) ("tags" + i).hashCode(), + System.currentTimeMillis(), + i, + null, + UUID.randomUUID().toString(), + 0, + 0, + properties + ); + + calcBitMap.dispatch(dispatchRequest); + + assertThat(dispatchRequest.getBitMap()).isNotNull(); + + BitsArray bits = BitsArray.create(dispatchRequest.getBitMap()); + + Collection filterDatas = filterManager.get(topic); + + for (ConsumerFilterData filterData : filterDatas) { + + if (filterManager.getBloomFilter().isHit(filterData.getBloomFilterData(), bits)) { + try { + assertThat((Boolean) filterData.getCompiledExpression().evaluate( + new MessageEvaluationContext(properties) + )).isTrue(); + } catch (Exception e) { + e.printStackTrace(); + assertThat(true).isFalse(); + } + } else { + try { + assertThat((Boolean) filterData.getCompiledExpression().evaluate( + new MessageEvaluationContext(properties) + )).isFalse(); + } catch (Exception e) { + e.printStackTrace(); + assertThat(true).isFalse(); + } + } + } + } + } +} 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 new file mode 100644 index 0000000..c01d829 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/filter/ConsumerFilterManagerTest.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.broker.filter; + +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; + +public class ConsumerFilterManagerTest { + + public static ConsumerFilterManager gen(int topicCount, int consumerCount) { + ConsumerFilterManager filterManager = new ConsumerFilterManager(); + + for (int i = 0; i < topicCount; i++) { + String topic = "topic" + i; + + for (int j = 0; j < consumerCount; j++) { + + String consumer = "CID_" + j; + + filterManager.register(topic, consumer, expr(j), ExpressionType.SQL92, System.currentTimeMillis()); + } + } + + return filterManager; + } + + public static String expr(int i) { + return "a is not null and a > " + ((i - 1) * 10) + " and a < " + ((i + 1) * 10); + } + + @Test + public void testRegister_newExpressionCompileErrorAndRemoveOld() { + ConsumerFilterManager filterManager = gen(10, 10); + + assertThat(filterManager.get("topic9", "CID_9")).isNotNull(); + + String newExpr = "a between 10,20"; + + assertThat(filterManager.register("topic9", "CID_9", newExpr, ExpressionType.SQL92, System.currentTimeMillis() + 1)) + .isFalse(); + assertThat(filterManager.get("topic9", "CID_9")).isNull(); + + newExpr = "a between 10 AND 20"; + + assertThat(filterManager.register("topic9", "CID_9", newExpr, ExpressionType.SQL92, System.currentTimeMillis() + 1)) + .isTrue(); + + ConsumerFilterData filterData = filterManager.get("topic9", "CID_9"); + + assertThat(filterData).isNotNull(); + assertThat(newExpr).isEqualTo(filterData.getExpression()); + } + + @Test + public void testRegister_change() { + ConsumerFilterManager filterManager = gen(10, 10); + + ConsumerFilterData filterData; + + String newExpr = "a > 0 and a < 10"; + + filterManager.register("topic9", "CID_9", newExpr, ExpressionType.SQL92, System.currentTimeMillis() + 1); + + filterData = filterManager.get("topic9", "CID_9"); + + assertThat(newExpr).isEqualTo(filterData.getExpression()); + } + + @Test + public void testRegister() { + ConsumerFilterManager filterManager = gen(10, 10); + + ConsumerFilterData filterData = filterManager.get("topic9", "CID_9"); + + assertThat(filterData).isNotNull(); + assertThat(filterData.isDead()).isFalse(); + + // new version + assertThat(filterManager.register( + "topic9", "CID_9", "a is not null", ExpressionType.SQL92, System.currentTimeMillis() + 1000 + )).isTrue(); + + ConsumerFilterData newFilter = filterManager.get("topic9", "CID_9"); + + assertThat(newFilter).isNotEqualTo(filterData); + + // same version + assertThat(filterManager.register( + "topic9", "CID_9", "a is null", ExpressionType.SQL92, newFilter.getClientVersion() + )).isFalse(); + + ConsumerFilterData filterData1 = filterManager.get("topic9", "CID_9"); + + assertThat(newFilter).isEqualTo(filterData1); + } + + @Test + public void testRegister_reAlive() { + ConsumerFilterManager filterManager = gen(10, 10); + + ConsumerFilterData filterData = filterManager.get("topic9", "CID_9"); + + assertThat(filterData).isNotNull(); + assertThat(filterData.isDead()).isFalse(); + + //make dead + filterManager.unRegister("CID_9"); + + //reAlive + filterManager.register( + filterData.getTopic(), + filterData.getConsumerGroup(), + filterData.getExpression(), + filterData.getExpressionType(), + System.currentTimeMillis() + ); + + ConsumerFilterData newFilterData = filterManager.get("topic9", "CID_9"); + + assertThat(newFilterData).isNotNull(); + assertThat(newFilterData.isDead()).isFalse(); + } + + @Test + public void testRegister_bySubscriptionData() { + ConsumerFilterManager filterManager = new ConsumerFilterManager(); + List subscriptionDatas = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + try { + subscriptionDatas.add( + FilterAPI.build( + "topic" + i, + "a is not null and a > " + i, + ExpressionType.SQL92 + ) + ); + } catch (Exception e) { + e.printStackTrace(); + assertThat(true).isFalse(); + } + } + + filterManager.register("CID_0", subscriptionDatas); + + Collection filterDatas = filterManager.getByGroup("CID_0"); + + assertThat(filterDatas).isNotNull(); + assertThat(filterDatas.size()).isEqualTo(10); + + Iterator iterator = filterDatas.iterator(); + while (iterator.hasNext()) { + ConsumerFilterData filterData = iterator.next(); + + assertThat(filterData).isNotNull(); + assertThat(filterManager.getBloomFilter().isValid(filterData.getBloomFilterData())).isTrue(); + } + } + + @Test + public void testRegister_tag() { + ConsumerFilterManager filterManager = new ConsumerFilterManager(); + + assertThat(filterManager.register("topic0", "CID_0", "*", null, System.currentTimeMillis())).isFalse(); + + Collection filterDatas = filterManager.getByGroup("CID_0"); + + assertThat(filterDatas).isNullOrEmpty(); + } + + @Test + public void testUnregister() { + ConsumerFilterManager filterManager = gen(10, 10); + + ConsumerFilterData filterData = filterManager.get("topic9", "CID_9"); + + assertThat(filterData).isNotNull(); + assertThat(filterData.isDead()).isFalse(); + + filterManager.unRegister("CID_9"); + + assertThat(filterData.isDead()).isTrue(); + } + + @Test + public void testPersist() { + ConsumerFilterManager filterManager = gen(10, 10); + + try { + filterManager.persist(); + + ConsumerFilterData filterData = filterManager.get("topic9", "CID_9"); + + assertThat(filterData).isNotNull(); + assertThat(filterData.isDead()).isFalse(); + + ConsumerFilterManager loadFilter = new ConsumerFilterManager(); + + assertThat(loadFilter.load()).isTrue(); + + filterData = loadFilter.get("topic9", "CID_9"); + + assertThat(filterData).isNotNull(); + assertThat(filterData.isDead()).isTrue(); + assertThat(filterData.getCompiledExpression()).isNotNull(); + } finally { + UtilAll.deleteFile(new File("./unit_test")); + } + } + + @Test + public void testPersist_clean() { + ConsumerFilterManager filterManager = gen(10, 10); + + String topic = "topic9"; + for (int i = 0; i < 10; i++) { + String cid = "CID_" + i; + + ConsumerFilterData filterData = filterManager.get(topic, cid); + + assertThat(filterData).isNotNull(); + assertThat(filterData.isDead()).isFalse(); + + //make dead more than 24h + filterData.setBornTime(System.currentTimeMillis() - 26 * 60 * 60 * 1000); + filterData.setDeadTime(System.currentTimeMillis() - 25 * 60 * 60 * 1000); + } + + try { + filterManager.persist(); + + ConsumerFilterManager loadFilter = new ConsumerFilterManager(); + + assertThat(loadFilter.load()).isTrue(); + + ConsumerFilterData filterData = loadFilter.get(topic, "CID_9"); + + assertThat(filterData).isNull(); + + Collection topicData = loadFilter.get(topic); + + assertThat(topicData).isNullOrEmpty(); + } finally { + UtilAll.deleteFile(new File("./unit_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 new file mode 100644 index 0000000..84bca91 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/filter/MessageStoreWithFilterTest.java @@ -0,0 +1,388 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.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; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.GetMessageStatus; +import org.apache.rocketmq.store.MessageArrivingListener; +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 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[] MSG_BODY = MSG.getBytes(); + + 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 storeHost; + + private DefaultMessageStore master; + + private ConsumerFilterManager filterManager; + + private int topicCount = 3; + + private int msgPerTopic = 30; + + 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 init() throws Exception { + filterManager = ConsumerFilterManagerTest.gen(topicCount, msgPerTopic); + master = gen(filterManager); + } + + @After + public void destroy() { + if (master != null) { + master.shutdown(); + master.destroy(); + } + UtilAll.deleteFile(new File(STORE_PATH)); + } + + public MessageExtBrokerInner buildMessage() { + MessageExtBrokerInner msg = new MessageExtBrokerInner(); + msg.setTopic(TOPIC); + msg.setTags(System.currentTimeMillis() + "TAG"); + 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 = 1; i < 3; i++) { + msg.putUserProperty(String.valueOf(i), "imagoodperson" + i); + } + msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); + + return msg; + } + + 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(ConsumerFilterManager filterManager) throws Exception { + MessageStoreConfig messageStoreConfig = buildStoreConfig( + COMMIT_LOG_FILE_SIZE, CQ_FILE_SIZE, true, CQ_EXT_FILE_SIZE + ); + + BrokerConfig brokerConfig = new BrokerConfig(); + brokerConfig.setEnableCalcFilterBitMap(true); + brokerConfig.setMaxErrorRateOfBloomFilter(20); + brokerConfig.setExpectConsumerNumUseFilter(64); + + 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<>()); + + master.getDispatcherList().addFirst(new CommitLogDispatcher() { + @Override + public void dispatch(DispatchRequest request) { + try { + } catch (Throwable e) { + e.printStackTrace(); + } + } + }); + master.getDispatcherList().addFirst(new CommitLogDispatcherCalcBitMap(brokerConfig, filterManager)); + + if (MixAll.isWindows()) { + Assume.assumeTrue(master.load()); + } else { + assertThat(master.load()).isTrue(); + } + + master.start(); + + return master; + } + + protected List putMsg(DefaultMessageStore master, int topicCount, + int msgCountPerTopic) throws Exception { + List msgs = new ArrayList<>(); + for (int i = 0; i < topicCount; i++) { + String realTopic = TOPIC + i; + for (int j = 0; j < msgCountPerTopic; j++) { + MessageExtBrokerInner msg = buildMessage(); + msg.setTopic(realTopic); + msg.putUserProperty("a", String.valueOf(j * 10 + 5)); + msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); + + PutMessageResult result = master.putMessage(msg); + + msg.setMsgId(result.getAppendMessageResult().getMsgId()); + + msgs.add(msg); + } + } + + return msgs; + } + + protected List filtered(List msgs, ConsumerFilterData filterData) { + List filteredMsgs = new ArrayList<>(); + + for (MessageExtBrokerInner messageExtBrokerInner : msgs) { + + if (!messageExtBrokerInner.getTopic().equals(filterData.getTopic())) { + continue; + } + + try { + Object evlRet = filterData.getCompiledExpression().evaluate(new MessageEvaluationContext(messageExtBrokerInner.getProperties())); + + if (evlRet == null || !(evlRet instanceof Boolean) || (Boolean) evlRet) { + filteredMsgs.add(messageExtBrokerInner); + } + } catch (Exception e) { + e.printStackTrace(); + assertThat(true).isFalse(); + } + } + + return filteredMsgs; + } + + @Test + public void testGetMessage_withFilterBitMapAndConsumerChanged() throws Exception { + List msgs = putMsg(master, topicCount, msgPerTopic); + + Thread.sleep(200); + + // reset consumer; + String topic = "topic" + 0; + String resetGroup = "CID_" + 2; + String normalGroup = "CID_" + 3; + + { + // reset CID_2@topic0 to get all messages. + SubscriptionData resetSubData = new SubscriptionData(); + resetSubData.setExpressionType(ExpressionType.SQL92); + resetSubData.setTopic(topic); + resetSubData.setClassFilterMode(false); + resetSubData.setSubString("a is not null OR a is null"); + + ConsumerFilterData resetFilterData = ConsumerFilterManager.build(topic, + resetGroup, resetSubData.getSubString(), resetSubData.getExpressionType(), + System.currentTimeMillis()); + + GetMessageResult resetGetResult = master.getMessage(resetGroup, topic, QUEUE_ID, 0, 1000, + new ExpressionMessageFilter(resetSubData, resetFilterData, filterManager)); + + try { + assertThat(resetGetResult).isNotNull(); + + List filteredMsgs = filtered(msgs, resetFilterData); + + assertThat(resetGetResult.getMessageBufferList().size()).isEqualTo(filteredMsgs.size()); + } finally { + resetGetResult.release(); + } + } + + { + ConsumerFilterData normalFilterData = filterManager.get(topic, normalGroup); + assertThat(normalFilterData).isNotNull(); + assertThat(normalFilterData.getBornTime()).isLessThan(System.currentTimeMillis()); + + SubscriptionData normalSubData = new SubscriptionData(); + normalSubData.setExpressionType(normalFilterData.getExpressionType()); + normalSubData.setTopic(topic); + normalSubData.setClassFilterMode(false); + normalSubData.setSubString(normalFilterData.getExpression()); + + List filteredMsgs = filtered(msgs, normalFilterData); + + GetMessageResult normalGetResult = master.getMessage(normalGroup, topic, QUEUE_ID, 0, 1000, + new ExpressionMessageFilter(normalSubData, normalFilterData, filterManager)); + + try { + assertThat(normalGetResult).isNotNull(); + assertThat(normalGetResult.getMessageBufferList().size()).isEqualTo(filteredMsgs.size()); + } finally { + normalGetResult.release(); + } + } + } + + @Test + public void testGetMessage_withFilterBitMap() throws Exception { + List msgs = putMsg(master, topicCount, msgPerTopic); + + Thread.sleep(100); + + for (int i = 0; i < topicCount; i++) { + String realTopic = TOPIC + i; + + for (int j = 0; j < msgPerTopic; j++) { + String group = "CID_" + j; + + ConsumerFilterData filterData = filterManager.get(realTopic, group); + assertThat(filterData).isNotNull(); + + List filteredMsgs = filtered(msgs, filterData); + + SubscriptionData subscriptionData = new SubscriptionData(); + subscriptionData.setExpressionType(filterData.getExpressionType()); + subscriptionData.setTopic(filterData.getTopic()); + subscriptionData.setClassFilterMode(false); + subscriptionData.setSubString(filterData.getExpression()); + + GetMessageResult getMessageResult = master.getMessage(group, realTopic, QUEUE_ID, 0, 10000, + new ExpressionMessageFilter(subscriptionData, filterData, filterManager)); + String assertMsg = group + "-" + realTopic; + try { + assertThat(getMessageResult).isNotNull(); + assertThat(GetMessageStatus.FOUND).isEqualTo(getMessageResult.getStatus()); + assertThat(getMessageResult.getMessageBufferList()).isNotNull().isNotEmpty(); + assertThat(getMessageResult.getMessageBufferList().size()).isEqualTo(filteredMsgs.size()); + + for (ByteBuffer buffer : getMessageResult.getMessageBufferList()) { + MessageExt messageExt = MessageDecoder.decode(buffer.slice(), false); + assertThat(messageExt).isNotNull(); + + Object evlRet = null; + try { + evlRet = filterData.getCompiledExpression().evaluate(new MessageEvaluationContext(messageExt.getProperties())); + } catch (Exception e) { + e.printStackTrace(); + assertThat(true).isFalse(); + } + + assertThat(evlRet).isNotNull().isEqualTo(Boolean.TRUE); + + // check + boolean find = false; + for (MessageExtBrokerInner messageExtBrokerInner : filteredMsgs) { + if (messageExtBrokerInner.getMsgId().equals(messageExt.getMsgId())) { + find = true; + } + } + assertThat(find).isTrue(); + } + } finally { + getMessageResult.release(); + } + } + } + } + + @Test + public void testGetMessage_withFilter_checkTagsCode() throws Exception { + putMsg(master, topicCount, msgPerTopic); + + 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); + } + } + }); + } +} 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 new file mode 100644 index 0000000..2216a1d --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/latency/BrokerFastFailureTest.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.broker.latency; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.future.FutureTaskExt; +import org.apache.rocketmq.remoting.netty.RequestTask; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.MessageStore; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BrokerFastFailureTest { + + private BrokerController brokerController; + + private final BrokerConfig brokerConfig = new BrokerConfig(); + + private MessageStore messageStore; + + @Before + public void setUp() { + brokerController = Mockito.mock(BrokerController.class); + messageStore = Mockito.mock(DefaultMessageStore.class); + BlockingQueue queue = new LinkedBlockingQueue<>(); + Mockito.when(brokerController.getSendThreadPoolQueue()).thenReturn(queue); + Mockito.when(brokerController.getPullThreadPoolQueue()).thenReturn(queue); + Mockito.when(brokerController.getLitePullThreadPoolQueue()).thenReturn(queue); + Mockito.when(brokerController.getHeartbeatThreadPoolQueue()).thenReturn(queue); + Mockito.when(brokerController.getEndTransactionThreadPoolQueue()).thenReturn(queue); + Mockito.when(brokerController.getAdminBrokerThreadPoolQueue()).thenReturn(queue); + Mockito.when(brokerController.getAckThreadPoolQueue()).thenReturn(queue); + Mockito.when(brokerController.getBrokerConfig()).thenReturn(brokerConfig); + Mockito.when(messageStore.isOSPageCacheBusy()).thenReturn(false); + Mockito.when(brokerController.getMessageStore()).thenReturn(messageStore); + } + + @Test + public void testCleanExpiredRequestInQueue() throws Exception { + BrokerFastFailure brokerFastFailure = new BrokerFastFailure(brokerController); + + BlockingQueue queue = new LinkedBlockingQueue<>(); + brokerFastFailure.cleanExpiredRequestInQueue(queue, 1); + assertThat(queue.size()).isZero(); + + //Normal Runnable + Runnable runnable = new Runnable() { + @Override + public void run() { + + } + }; + queue.add(runnable); + + assertThat(queue.size()).isEqualTo(1); + brokerFastFailure.cleanExpiredRequestInQueue(queue, 1); + assertThat(queue.size()).isEqualTo(1); + + queue.clear(); + + //With expired request + RequestTask expiredRequest = new RequestTask(runnable, null, null); + queue.add(new FutureTaskExt<>(expiredRequest, null)); + TimeUnit.MILLISECONDS.sleep(100); + + RequestTask requestTask = new RequestTask(runnable, null, null); + queue.add(new FutureTaskExt<>(requestTask, null)); + + assertThat(queue.size()).isEqualTo(2); + brokerFastFailure.cleanExpiredRequestInQueue(queue, 100); + assertThat(queue.size()).isEqualTo(1); + assertThat(((FutureTaskExt) queue.peek()).getRunnable()).isEqualTo(requestTask); + } + + @Test + public void testCleanExpiredCustomRequestInQueue() throws Exception { + BrokerFastFailure brokerFastFailure = new BrokerFastFailure(brokerController); + brokerFastFailure.start(); + brokerConfig.setWaitTimeMillsInAckQueue(10); + BlockingQueue customThreadPoolQueue = new LinkedBlockingQueue<>(); + brokerFastFailure.addCleanExpiredRequestQueue(customThreadPoolQueue, () -> brokerConfig.getWaitTimeMillsInAckQueue()); + + Runnable runnable = new Runnable() { + @Override + public void run() { + + } + }; + RequestTask requestTask = new RequestTask(runnable, null, null); + customThreadPoolQueue.add(new FutureTaskExt<>(requestTask, null)); + + Thread.sleep(2000); + + assertThat(customThreadPoolQueue.size()).isEqualTo(0); + assertThat(requestTask.isStopRun()).isEqualTo(true); + + brokerConfig.setWaitTimeMillsInAckQueue(10000); + + RequestTask requestTask2 = new RequestTask(runnable, null, null); + customThreadPoolQueue.add(new FutureTaskExt<>(requestTask2, null)); + + Thread.sleep(1000); + + assertThat(customThreadPoolQueue.size()).isEqualTo(1); + assertThat(((FutureTaskExt) customThreadPoolQueue.peek()).getRunnable()).isEqualTo(requestTask2); + + brokerFastFailure.shutdown(); + + } + +} \ No newline at end of file diff --git a/broker/src/test/java/org/apache/rocketmq/broker/longpolling/PopLongPollingServiceTest.java b/broker/src/test/java/org/apache/rocketmq/broker/longpolling/PopLongPollingServiceTest.java new file mode 100644 index 0000000..003bf09 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/longpolling/PopLongPollingServiceTest.java @@ -0,0 +1,223 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Channel; +import io.netty.channel.ChannelHandlerContext; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.store.MessageFilter; +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 java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.ExecutorService; + +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.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class PopLongPollingServiceTest { + + @Mock + private BrokerController brokerController; + + @Mock + private NettyRequestProcessor processor; + + @Mock + private ChannelHandlerContext ctx; + + @Mock + private ExecutorService pullMessageExecutor; + + private PopLongPollingService popLongPollingService; + + private final String defaultTopic = "defaultTopic"; + + @Before + public void init() { + BrokerConfig brokerConfig = new BrokerConfig(); + brokerConfig.setPopPollingMapSize(100); + when(brokerController.getBrokerConfig()).thenReturn(brokerConfig); + popLongPollingService = spy(new PopLongPollingService(brokerController, processor, true)); + } + + @Test + public void testNotifyMessageArrivingWithRetryTopic() { + int queueId = 0; + doNothing().when(popLongPollingService).notifyMessageArrivingWithRetryTopic(defaultTopic, queueId, -1L, null, 0L, null, null); + popLongPollingService.notifyMessageArrivingWithRetryTopic(defaultTopic, queueId); + verify(popLongPollingService, times(1)).notifyMessageArrivingWithRetryTopic(defaultTopic, queueId, -1L, null, 0L, null, null); + } + + @Test + public void testNotifyMessageArriving() { + int queueId = 0; + Long tagsCode = 123L; + long offset = 123L; + long msgStoreTime = System.currentTimeMillis(); + byte[] filterBitMap = new byte[] {0x01}; + Map properties = new ConcurrentHashMap<>(); + doNothing().when(popLongPollingService).notifyMessageArriving(defaultTopic, queueId, offset, tagsCode, msgStoreTime, filterBitMap, properties); + popLongPollingService.notifyMessageArrivingWithRetryTopic(defaultTopic, queueId, offset, tagsCode, msgStoreTime, filterBitMap, properties); + verify(popLongPollingService).notifyMessageArriving(defaultTopic, queueId, offset, tagsCode, msgStoreTime, filterBitMap, properties); + } + + @Test + public void testNotifyMessageArrivingValidRequest() throws Exception { + String cid = "CID_1"; + int queueId = 0; + ConcurrentLinkedHashMap> topicCidMap = new ConcurrentLinkedHashMap.Builder>() + .maximumWeightedCapacity(10).build(); + ConcurrentHashMap cids = new ConcurrentHashMap<>(); + cids.put(cid, (byte) 1); + topicCidMap.put(defaultTopic, cids); + popLongPollingService = new PopLongPollingService(brokerController, processor, true); + ConcurrentLinkedHashMap> pollingMap = + new ConcurrentLinkedHashMap.Builder>().maximumWeightedCapacity(this.brokerController.getBrokerConfig().getPopPollingMapSize()).build(); + Channel channel = mock(Channel.class); + when(channel.isActive()).thenReturn(true); + PopRequest popRequest = mock(PopRequest.class); + MessageFilter messageFilter = mock(MessageFilter.class); + SubscriptionData subscriptionData = mock(SubscriptionData.class); + when(popRequest.getMessageFilter()).thenReturn(messageFilter); + when(popRequest.getSubscriptionData()).thenReturn(subscriptionData); + when(popRequest.getChannel()).thenReturn(channel); + String pollingKey = KeyBuilder.buildPollingKey(defaultTopic, cid, queueId); + ConcurrentSkipListSet popRequests = mock(ConcurrentSkipListSet.class); + when(popRequests.pollLast()).thenReturn(popRequest); + pollingMap.put(pollingKey, popRequests); + FieldUtils.writeDeclaredField(popLongPollingService, "topicCidMap", topicCidMap, true); + FieldUtils.writeDeclaredField(popLongPollingService, "pollingMap", pollingMap, true); + boolean actual = popLongPollingService.notifyMessageArriving(defaultTopic, queueId, cid, null, 0, null, null); + assertFalse(actual); + } + + @Test + public void testWakeUpNullRequest() { + assertFalse(popLongPollingService.wakeUp(null)); + } + + @Test + public void testWakeUpIncompleteRequest() { + PopRequest request = mock(PopRequest.class); + when(request.complete()).thenReturn(false); + assertFalse(popLongPollingService.wakeUp(request)); + } + + @Test + public void testWakeUpInactiveChannel() { + PopRequest request = mock(PopRequest.class); + when(request.complete()).thenReturn(true); + when(request.getCtx()).thenReturn(ctx); + Channel channel = mock(Channel.class); + when(ctx.channel()).thenReturn(channel); + when(channel.isActive()).thenReturn(true); + when(brokerController.getPullMessageExecutor()).thenReturn(pullMessageExecutor); + assertTrue(popLongPollingService.wakeUp(request)); + } + + @Test + public void testWakeUpValidRequestWithException() throws Exception { + PopRequest request = mock(PopRequest.class); + when(request.complete()).thenReturn(true); + when(request.getCtx()).thenReturn(ctx); + Channel channel = mock(Channel.class); + when(ctx.channel()).thenReturn(channel); + when(request.getChannel()).thenReturn(channel); + when(channel.isActive()).thenReturn(true); + when(brokerController.getPullMessageExecutor()).thenReturn(pullMessageExecutor); + when(processor.processRequest(any(), any())).thenThrow(new RuntimeException("Test Exception")); + assertTrue(popLongPollingService.wakeUp(request)); + ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + verify(pullMessageExecutor).submit(captor.capture()); + captor.getValue().run(); + verify(processor).processRequest(any(), any()); + } + + @Test + public void testPollingNotPolling() { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + RemotingCommand remotingCommand = mock(RemotingCommand.class); + PollingHeader requestHeader = mock(PollingHeader.class); + SubscriptionData subscriptionData = mock(SubscriptionData.class); + MessageFilter messageFilter = mock(MessageFilter.class); + when(requestHeader.getPollTime()).thenReturn(0L); + PollingResult result = popLongPollingService.polling(ctx, remotingCommand, requestHeader, subscriptionData, messageFilter); + assertEquals(PollingResult.NOT_POLLING, result); + } + + @Test + public void testPollingServicePollingTimeout() throws IllegalAccessException { + String cid = "CID_1"; + popLongPollingService = new PopLongPollingService(brokerController, processor, true); + popLongPollingService.shutdown(); + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + RemotingCommand remotingCommand = mock(RemotingCommand.class); + PollingHeader requestHeader = mock(PollingHeader.class); + SubscriptionData subscriptionData = mock(SubscriptionData.class); + MessageFilter messageFilter = mock(MessageFilter.class); + when(requestHeader.getPollTime()).thenReturn(1000L); + when(requestHeader.getTopic()).thenReturn(defaultTopic); + when(requestHeader.getConsumerGroup()).thenReturn("defaultGroup"); + ConcurrentLinkedHashMap> topicCidMap = new ConcurrentLinkedHashMap.Builder>() + .maximumWeightedCapacity(10).build(); + ConcurrentHashMap cids = new ConcurrentHashMap<>(); + cids.put(cid, (byte) 1); + topicCidMap.put(defaultTopic, cids); + FieldUtils.writeDeclaredField(popLongPollingService, "topicCidMap", topicCidMap, true); + PollingResult result = popLongPollingService.polling(ctx, remotingCommand, requestHeader, subscriptionData, messageFilter); + assertEquals(PollingResult.POLLING_TIMEOUT, result); + } + + @Test + public void testPollingPollingSuc() { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + RemotingCommand remotingCommand = mock(RemotingCommand.class); + PollingHeader requestHeader = mock(PollingHeader.class); + SubscriptionData subscriptionData = mock(SubscriptionData.class); + MessageFilter messageFilter = mock(MessageFilter.class); + when(requestHeader.getPollTime()).thenReturn(1000L); + when(requestHeader.getBornTime()).thenReturn(System.currentTimeMillis()); + when(requestHeader.getTopic()).thenReturn("topic"); + when(requestHeader.getConsumerGroup()).thenReturn("cid"); + when(requestHeader.getQueueId()).thenReturn(0); + PollingResult result = popLongPollingService.polling(ctx, remotingCommand, requestHeader, subscriptionData, messageFilter); + assertEquals(PollingResult.POLLING_SUC, result); + } +} 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 0000000..6eeb4ad --- /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 0000000..9264eb4 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/metrics/BrokerMetricsManagerTest.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.broker.metrics; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.attribute.TopicMessageType; +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.topic.TopicValidator; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.Test; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +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"); + } + + + @Test + public void testIsRetryOrDlqTopicWithRetryTopic() { + String topic = MixAll.RETRY_GROUP_TOPIC_PREFIX + "TestTopic"; + boolean result = BrokerMetricsManager.isRetryOrDlqTopic(topic); + assertThat(result).isTrue(); + } + + @Test + public void testIsRetryOrDlqTopicWithDlqTopic() { + String topic = MixAll.DLQ_GROUP_TOPIC_PREFIX + "TestTopic"; + boolean result = BrokerMetricsManager.isRetryOrDlqTopic(topic); + assertThat(result).isTrue(); + } + + @Test + public void testIsRetryOrDlqTopicWithNonRetryOrDlqTopic() { + String topic = "NormalTopic"; + boolean result = BrokerMetricsManager.isRetryOrDlqTopic(topic); + assertThat(result).isFalse(); + } + + @Test + public void testIsRetryOrDlqTopicWithEmptyTopic() { + String topic = ""; + boolean result = BrokerMetricsManager.isRetryOrDlqTopic(topic); + assertThat(result).isFalse(); + } + + @Test + public void testIsRetryOrDlqTopicWithNullTopic() { + String topic = null; + boolean result = BrokerMetricsManager.isRetryOrDlqTopic(topic); + assertThat(result).isFalse(); + } + + @Test + public void testIsSystemGroup_SystemGroup_ReturnsTrue() { + String group = "FooGroup"; + String systemGroup = MixAll.CID_RMQ_SYS_PREFIX + group; + boolean result = BrokerMetricsManager.isSystemGroup(systemGroup); + assertThat(result).isTrue(); + } + + @Test + public void testIsSystemGroup_NonSystemGroup_ReturnsFalse() { + String group = "FooGroup"; + boolean result = BrokerMetricsManager.isSystemGroup(group); + assertThat(result).isFalse(); + } + + @Test + public void testIsSystemGroup_EmptyGroup_ReturnsFalse() { + String group = ""; + boolean result = BrokerMetricsManager.isSystemGroup(group); + assertThat(result).isFalse(); + } + + @Test + public void testIsSystemGroup_NullGroup_ReturnsFalse() { + String group = null; + boolean result = BrokerMetricsManager.isSystemGroup(group); + assertThat(result).isFalse(); + } + + @Test + public void testIsSystem_SystemTopicOrSystemGroup_ReturnsTrue() { + String topic = "FooTopic"; + String group = "FooGroup"; + String systemTopic = TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC; + String systemGroup = MixAll.CID_RMQ_SYS_PREFIX + group; + + boolean resultTopic = BrokerMetricsManager.isSystem(systemTopic, group); + assertThat(resultTopic).isTrue(); + + boolean resultGroup = BrokerMetricsManager.isSystem(topic, systemGroup); + assertThat(resultGroup).isTrue(); + } + + @Test + public void testIsSystem_NonSystemTopicAndGroup_ReturnsFalse() { + String topic = "FooTopic"; + String group = "FooGroup"; + boolean result = BrokerMetricsManager.isSystem(topic, group); + assertThat(result).isFalse(); + } + + @Test + public void testIsSystem_EmptyTopicAndGroup_ReturnsFalse() { + String topic = ""; + String group = ""; + boolean result = BrokerMetricsManager.isSystem(topic, group); + assertThat(result).isFalse(); + } + + @Test + public void testGetMessageTypeAsNormal() { + SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); + requestHeader.setProperties(""); + + TopicMessageType result = BrokerMetricsManager.getMessageType(requestHeader); + assertThat(TopicMessageType.NORMAL).isEqualTo(result); + } + + @Test + public void testGetMessageTypeAsTransaction() { + SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); + + Map map = new HashMap<>(); + map.put(MessageConst.PROPERTY_TRANSACTION_PREPARED, "true"); + requestHeader.setProperties(MessageDecoder.messageProperties2String(map)); + + TopicMessageType result = BrokerMetricsManager.getMessageType(requestHeader); + assertThat(TopicMessageType.TRANSACTION).isEqualTo(result); + } + + @Test + public void testGetMessageTypeAsFifo() { + SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); + Map map = new HashMap<>(); + map.put(MessageConst.PROPERTY_SHARDING_KEY, "shardingKey"); + requestHeader.setProperties(MessageDecoder.messageProperties2String(map)); + + TopicMessageType result = BrokerMetricsManager.getMessageType(requestHeader); + assertThat(TopicMessageType.FIFO).isEqualTo(result); + } + + @Test + public void testGetMessageTypeAsDelayLevel() { + SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); + Map map = new HashMap<>(); + map.put(MessageConst.PROPERTY_DELAY_TIME_LEVEL, "1"); + requestHeader.setProperties(MessageDecoder.messageProperties2String(map)); + + TopicMessageType result = BrokerMetricsManager.getMessageType(requestHeader); + assertThat(TopicMessageType.DELAY).isEqualTo(result); + } + + @Test + public void testGetMessageTypeAsDeliverMS() { + SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); + Map map = new HashMap<>(); + map.put(MessageConst.PROPERTY_TIMER_DELIVER_MS, "10"); + requestHeader.setProperties(MessageDecoder.messageProperties2String(map)); + + TopicMessageType result = BrokerMetricsManager.getMessageType(requestHeader); + assertThat(TopicMessageType.DELAY).isEqualTo(result); + } + + @Test + public void testGetMessageTypeAsDelaySEC() { + SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); + Map map = new HashMap<>(); + map.put(MessageConst.PROPERTY_TIMER_DELAY_SEC, "1"); + requestHeader.setProperties(MessageDecoder.messageProperties2String(map)); + + TopicMessageType result = BrokerMetricsManager.getMessageType(requestHeader); + assertThat(TopicMessageType.DELAY).isEqualTo(result); + } + + @Test + public void testGetMessageTypeAsDelayMS() { + SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); + Map map = new HashMap<>(); + map.put(MessageConst.PROPERTY_TIMER_DELAY_MS, "10"); + requestHeader.setProperties(MessageDecoder.messageProperties2String(map)); + + TopicMessageType result = BrokerMetricsManager.getMessageType(requestHeader); + assertThat(TopicMessageType.DELAY).isEqualTo(result); + } + + @Test + public void testGetMessageTypeWithUnknownProperty() { + SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); + Map map = new HashMap<>(); + map.put("unknownProperty", "unknownValue"); + requestHeader.setProperties(MessageDecoder.messageProperties2String(map)); + + TopicMessageType result = BrokerMetricsManager.getMessageType(requestHeader); + assertThat(TopicMessageType.NORMAL).isEqualTo(result); + } + + @Test + public void testGetMessageTypeWithMultipleProperties() { + SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); + Map map = new HashMap<>(); + map.put(MessageConst.PROPERTY_DELAY_TIME_LEVEL, "1"); + map.put(MessageConst.PROPERTY_SHARDING_KEY, "shardingKey"); + requestHeader.setProperties(MessageDecoder.messageProperties2String(map)); + + TopicMessageType result = BrokerMetricsManager.getMessageType(requestHeader); + assertThat(TopicMessageType.FIFO).isEqualTo(result); + } + + @Test + public void testGetMessageTypeWithTransactionFlagButOtherPropertiesPresent() { + SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); + Map map = new HashMap<>(); + map.put(MessageConst.PROPERTY_TRANSACTION_PREPARED, "true"); + map.put(MessageConst.PROPERTY_SHARDING_KEY, "shardingKey"); + requestHeader.setProperties(MessageDecoder.messageProperties2String(map)); + + TopicMessageType result = BrokerMetricsManager.getMessageType(requestHeader); + assertThat(TopicMessageType.TRANSACTION).isEqualTo(result); + } + + @Test + public void testGetMessageTypeWithEmptyProperties() { + TopicMessageType result = BrokerMetricsManager.getMessageType(new SendMessageRequestHeader()); + assertThat(TopicMessageType.NORMAL).isEqualTo(result); + } + + @Test + public void testCreateMetricsManager() { + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + String storePathRootDir = System.getProperty("java.io.tmpdir") + File.separator + "store-" + + UUID.randomUUID(); + messageStoreConfig.setStorePathRootDir(storePathRootDir); + BrokerConfig brokerConfig = new BrokerConfig(); + + NettyServerConfig nettyServerConfig = new NettyServerConfig(); + nettyServerConfig.setListenPort(0); + + BrokerController brokerController = new BrokerController(brokerConfig, nettyServerConfig, + new NettyClientConfig(), messageStoreConfig); + + BrokerMetricsManager metricsManager = new BrokerMetricsManager(brokerController); + + assertThat(metricsManager.getBrokerMeter()).isNull(); + } + + @Test + public void testCreateMetricsManagerLogType() throws CloneNotSupportedException { + BrokerConfig brokerConfig = new BrokerConfig(); + brokerConfig.setMetricsExporterType(MetricsExporterType.LOG); + brokerConfig.setMetricsLabel("label1:value1;label2:value2"); + brokerConfig.setMetricsOtelCardinalityLimit(1); + + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + String storePathRootDir = System.getProperty("java.io.tmpdir") + File.separator + "store-" + + UUID.randomUUID(); + messageStoreConfig.setStorePathRootDir(storePathRootDir); + + NettyServerConfig nettyServerConfig = new NettyServerConfig(); + nettyServerConfig.setListenPort(0); + + BrokerController brokerController = new BrokerController(brokerConfig, nettyServerConfig, + new NettyClientConfig(), messageStoreConfig); + brokerController.initialize(); + + BrokerMetricsManager metricsManager = new BrokerMetricsManager(brokerController); + + assertThat(metricsManager.getBrokerMeter()).isNotNull(); + } +} \ 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 0000000..ad5af92 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/offset/BroadcastOffsetManagerTest.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.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.apache.rocketmq.store.exception.ConsumeQueueException; +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() throws ConsumeQueueException { + 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() throws ConsumeQueueException { + // 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(); + }); + } +} 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 0000000..ef830b9 --- /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 0000000..d980090 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/offset/ConsumerOffsetManagerTest.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 org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.BrokerConfig; +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.apache.rocketmq.broker.offset.ConsumerOffsetManager.TOPIC_GROUP_SEPARATOR; +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 removeOffsetByGroupTest() { + String topic = "TopicName"; + String group = "GroupName"; + Mockito.when(brokerController.getBrokerConfig()).thenReturn(new BrokerConfig()); + consumerOffsetManager.commitOffset("Commit", group, topic, 0, 100); + consumerOffsetManager.assignResetOffset(topic, group, 0, 100); + consumerOffsetManager.commitPullOffset("Pull", group, topic, 0, 100); + consumerOffsetManager.removeOffset(group); + Assert.assertFalse(consumerOffsetManager.getOffsetTable().containsKey(topic + TOPIC_GROUP_SEPARATOR + group)); + + consumerOffsetManager.commitPullOffset("Pull", group, topic, 0, 100); + consumerOffsetManager.clearPullOffset(group, topic); + Assert.assertEquals(-1L, consumerOffsetManager.queryPullOffset(group, topic, 0)); + } + + @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 0000000..1fdf454 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/offset/ConsumerOrderInfoManagerLockFreeNotifyTest.java @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.apache.rocketmq.store.exception.ConsumeQueueException; +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() throws ConsumeQueueException { + 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()); + } +} 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 0000000..4414eda --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/offset/ConsumerOrderInfoManagerTest.java @@ -0,0 +1,533 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.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); + when(subscriptionGroupManager.containsSubscriptionGroup(GROUP)).thenReturn(true); + + 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 0000000..9626bca --- /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 0000000..5a7a2c3 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/offset/RocksDBConsumerOffsetManagerTest.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.broker.offset; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.config.v1.RocksDBConsumerOffsetManager; +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/offset/RocksDBLmqConsumerOffsetManagerTest.java b/broker/src/test/java/org/apache/rocketmq/broker/offset/RocksDBLmqConsumerOffsetManagerTest.java new file mode 100644 index 0000000..aa5003f --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/offset/RocksDBLmqConsumerOffsetManagerTest.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.broker.offset; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.config.v1.RocksDBConsumerOffsetManager; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.when; + +public class RocksDBLmqConsumerOffsetManagerTest { + private static final String LMQ_GROUP = MixAll.LMQ_PREFIX + "FooBarGroup"; + private static final String NON_LMQ_GROUP = "nonLmqGroup"; + + private static final String LMQ_TOPIC = MixAll.LMQ_PREFIX + "FooBarTopic"; + private static final String NON_LMQ_TOPIC = "FooBarTopic"; + private static final int QUEUE_ID = 0; + private static final long OFFSET = 12345; + + private BrokerController brokerController; + + private RocksDBConsumerOffsetManager offsetManager; + + @Before + public void setUp() { + brokerController = Mockito.mock(BrokerController.class); + when(brokerController.getMessageStoreConfig()).thenReturn(Mockito.mock(MessageStoreConfig.class)); + when(brokerController.getBrokerConfig()).thenReturn(new BrokerConfig()); + offsetManager = new RocksDBConsumerOffsetManager(brokerController); + } + + + @Test + public void testQueryOffsetForNonLmq() { + long actualOffset = offsetManager.queryOffset(NON_LMQ_GROUP, NON_LMQ_TOPIC, QUEUE_ID); + // Verify + assertEquals("Offset should not be null.", -1, actualOffset); + } + + + @Test + public void testQueryOffsetForLmqGroupWithExistingOffset() { + offsetManager.commitOffset("127.0.0.1",LMQ_GROUP, LMQ_TOPIC, QUEUE_ID, OFFSET); + + // Act + Map actualOffsets = offsetManager.queryOffset(LMQ_GROUP, LMQ_TOPIC); + + // Assert + assertNotNull(actualOffsets); + assertEquals(1, actualOffsets.size()); + assertEquals(OFFSET, (long) actualOffsets.get(0)); + } + + @Test + public void testQueryOffsetForLmqGroupWithoutExistingOffset() { + // Act + Map actualOffsets = offsetManager.queryOffset(LMQ_GROUP, "nonExistingTopic"); + // Assert + assertNull(actualOffsets); + } + + @Test + public void testQueryOffsetForNonLmqGroup() { + // Arrange + Map mockOffsets = new HashMap<>(); + mockOffsets.put(QUEUE_ID, OFFSET); + + offsetManager.commitOffset("clientHost", NON_LMQ_GROUP, NON_LMQ_TOPIC, QUEUE_ID, OFFSET); + + // Act + Map actualOffsets = offsetManager.queryOffset(NON_LMQ_GROUP, NON_LMQ_TOPIC); + + // Assert + assertNotNull(actualOffsets); + assertEquals("Offsets should match the mocked return value for non-LMQ groups", mockOffsets, actualOffsets); + } + + @Test + public void testCommitOffsetForLmq() { + // Execute + offsetManager.commitOffset("clientHost", LMQ_GROUP, LMQ_TOPIC, QUEUE_ID, OFFSET); + // Verify + Long expectedOffset = offsetManager.getOffsetTable().get(getLMQKey()).get(QUEUE_ID); + assertEquals("Offset should be updated correctly.", OFFSET, expectedOffset.longValue()); + } + + private String getLMQKey() { + return LMQ_TOPIC + "@" + LMQ_GROUP; + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/offset/RocksDBOffsetSerializeWrapperTest.java b/broker/src/test/java/org/apache/rocketmq/broker/offset/RocksDBOffsetSerializeWrapperTest.java new file mode 100644 index 0000000..c01e63f --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/offset/RocksDBOffsetSerializeWrapperTest.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.broker.offset; + + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.rocketmq.broker.config.v1.RocksDBOffsetSerializeWrapper; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class RocksDBOffsetSerializeWrapperTest { + + private RocksDBOffsetSerializeWrapper wrapper; + + @Before + public void setUp() { + wrapper = new RocksDBOffsetSerializeWrapper(); + } + + @Test + public void testGetOffsetTable_ShouldReturnConcurrentHashMap() { + ConcurrentMap offsetTable = wrapper.getOffsetTable(); + assertNotNull("The offsetTable should not be null", offsetTable); + } + + @Test + public void testSetOffsetTable_ShouldSetTheOffsetTableCorrectly() { + ConcurrentMap newOffsetTable = new ConcurrentHashMap<>(); + wrapper.setOffsetTable(newOffsetTable); + ConcurrentMap offsetTable = wrapper.getOffsetTable(); + assertEquals("The offsetTable should be the same as the one set", newOffsetTable, offsetTable); + } + +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/offset/RocksdbTransferOffsetAndCqTest.java b/broker/src/test/java/org/apache/rocketmq/broker/offset/RocksdbTransferOffsetAndCqTest.java new file mode 100644 index 0000000..6a805b0 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/offset/RocksdbTransferOffsetAndCqTest.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.broker.offset; + +import java.io.IOException; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import org.apache.commons.collections.MapUtils; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.config.v1.RocksDBConsumerOffsetManager; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.RocksDBMessageStore; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.queue.ConsumeQueueInterface; +import org.apache.rocketmq.store.queue.ConsumeQueueStoreInterface; +import org.apache.rocketmq.store.queue.CqUnit; +import org.apache.rocketmq.store.stats.BrokerStatsManager; +import org.awaitility.Awaitility; +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; +import org.rocksdb.RocksDBException; + +@RunWith(MockitoJUnitRunner.class) +public class RocksdbTransferOffsetAndCqTest { + + private final String basePath = Paths.get(System.getProperty("user.home"), + "unit-test-store", UUID.randomUUID().toString().substring(0, 16).toUpperCase()).toString(); + + private final String topic = "topic"; + private final String group = "group"; + private final String clientHost = "clientHost"; + private final int queueId = 1; + + private RocksDBConsumerOffsetManager rocksdbConsumerOffsetManager; + + private ConsumerOffsetManager consumerOffsetManager; + + private DefaultMessageStore defaultMessageStore; + + @Mock + private BrokerController brokerController; + + @Before + public void init() throws IOException { + if (notToBeExecuted()) { + return; + } + BrokerConfig brokerConfig = new BrokerConfig(); + brokerConfig.setConsumerOffsetUpdateVersionStep(10); + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setStorePathRootDir(basePath); + messageStoreConfig.setRocksdbCQDoubleWriteEnable(true); + Mockito.lenient().when(brokerController.getBrokerConfig()).thenReturn(brokerConfig); + Mockito.lenient().when(brokerController.getMessageStoreConfig()).thenReturn(messageStoreConfig); + + defaultMessageStore = new DefaultMessageStore(messageStoreConfig, new BrokerStatsManager("aaa", true), null, + brokerConfig, new ConcurrentHashMap()); + defaultMessageStore.enableRocksdbCQWrite(); + defaultMessageStore.loadCheckPoint(); + + consumerOffsetManager = new ConsumerOffsetManager(brokerController); + consumerOffsetManager.load(); + + rocksdbConsumerOffsetManager = new RocksDBConsumerOffsetManager(brokerController); + } + + @Test + public void testTransferOffset() { + if (notToBeExecuted()) { + return; + } + + for (int i = 0; i < 200; i++) { + consumerOffsetManager.commitOffset(clientHost, group, topic, queueId, i); + } + + ConcurrentMap> offsetTable = consumerOffsetManager.getOffsetTable(); + ConcurrentMap map = offsetTable.get(topic + "@" + group); + Assert.assertTrue(MapUtils.isNotEmpty(map)); + + Long offset = map.get(queueId); + Assert.assertEquals(199L, (long) offset); + + long offsetDataVersion = consumerOffsetManager.getDataVersion().getCounter().get(); + Assert.assertEquals(20L, offsetDataVersion); + + consumerOffsetManager.persist(); + + boolean loadResult = rocksdbConsumerOffsetManager.load(); + Assert.assertTrue(loadResult); + + ConcurrentMap> rocksdbOffsetTable = rocksdbConsumerOffsetManager.getOffsetTable(); + + ConcurrentMap rocksdbMap = rocksdbOffsetTable.get(topic + "@" + group); + Assert.assertTrue(MapUtils.isNotEmpty(rocksdbMap)); + + Long aLong1 = rocksdbMap.get(queueId); + Assert.assertEquals(199L, (long) aLong1); + + long rocksdbOffset = rocksdbConsumerOffsetManager.getDataVersion().getCounter().get(); + Assert.assertEquals(21L, rocksdbOffset); + } + + @Test + public void testRocksdbCqWrite() throws RocksDBException { + if (notToBeExecuted()) { + return; + } + RocksDBMessageStore kvStore = defaultMessageStore.getRocksDBMessageStore(); + ConsumeQueueStoreInterface store = kvStore.getConsumeQueueStore(); + store.start(); + ConsumeQueueInterface rocksdbCq = defaultMessageStore.getRocksDBMessageStore().findConsumeQueue(topic, queueId); + ConsumeQueueInterface fileCq = defaultMessageStore.findConsumeQueue(topic, queueId); + for (int i = 0; i < 200; i++) { + DispatchRequest request = new DispatchRequest(topic, queueId, i, 200, 0, System.currentTimeMillis(), i, "", "", 0, 0, new HashMap<>()); + fileCq.putMessagePositionInfoWrapper(request); + store.putMessagePositionInfoWrapper(request); + } + Awaitility.await() + .pollInterval(100, TimeUnit.MILLISECONDS) + .atMost(3, TimeUnit.SECONDS) + .until(() -> rocksdbCq.getMaxOffsetInQueue() == 200); + Pair unit = rocksdbCq.getCqUnitAndStoreTime(100); + Pair unit1 = fileCq.getCqUnitAndStoreTime(100); + Assert.assertEquals(unit.getObject1().getPos(), unit1.getObject1().getPos()); + } + + /** + * No need to skip macOS platform. + * @return true if some platform is NOT a good fit for this test case. + */ + private boolean notToBeExecuted() { + return false; + } + +} 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 0000000..2617b5c --- /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 0000000..1930641 --- /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/pop/PopConsumerCacheTest.java b/broker/src/test/java/org/apache/rocketmq/broker/pop/PopConsumerCacheTest.java new file mode 100644 index 0000000..3f6e893 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/pop/PopConsumerCacheTest.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.broker.pop; + +import java.util.Collections; +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.offset.ConsumerOffsetManager; +import org.apache.rocketmq.common.BrokerConfig; +import org.awaitility.Awaitility; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import static org.mockito.ArgumentMatchers.any; + +public class PopConsumerCacheTest { + + private final String attemptId = "attemptId"; + private final String topicId = "TopicTest"; + private final String groupId = "GroupTest"; + private final int queueId = 2; + + @Test + public void consumerRecordsTest() { + BrokerConfig brokerConfig = new BrokerConfig(); + brokerConfig.setPopConsumerKVServiceLog(true); + PopConsumerCache.ConsumerRecords consumerRecords = + new PopConsumerCache.ConsumerRecords(brokerConfig, groupId, topicId, queueId); + Assert.assertNotNull(consumerRecords.toString()); + + for (int i = 0; i < 5; i++) { + consumerRecords.write(new PopConsumerRecord(i, groupId, topicId, queueId, 0, + 20000, 100 + i, attemptId)); + } + Assert.assertEquals(100, consumerRecords.getMinOffsetInBuffer()); + Assert.assertEquals(5, consumerRecords.getInFlightRecordCount()); + + for (int i = 0; i < 2; i++) { + consumerRecords.delete(new PopConsumerRecord(i, groupId, topicId, queueId, 0, + 20000, 100 + i, attemptId)); + } + Assert.assertEquals(102, consumerRecords.getMinOffsetInBuffer()); + Assert.assertEquals(3, consumerRecords.getInFlightRecordCount()); + + long bufferTimeout = brokerConfig.getPopCkStayBufferTime(); + Assert.assertEquals(1, consumerRecords.removeExpiredRecords(bufferTimeout + 2).size()); + Assert.assertNull(consumerRecords.removeExpiredRecords(bufferTimeout + 2)); + Assert.assertEquals(2, consumerRecords.removeExpiredRecords(bufferTimeout + 4).size()); + Assert.assertNull(consumerRecords.removeExpiredRecords(bufferTimeout + 4)); + } + + @Test + public void consumerOffsetTest() throws IllegalAccessException { + BrokerController brokerController = Mockito.mock(BrokerController.class); + PopConsumerKVStore consumerKVStore = Mockito.mock(PopConsumerRocksdbStore.class); + PopConsumerLockService consumerLockService = Mockito.mock(PopConsumerLockService.class); + ConsumerOffsetManager consumerOffsetManager = Mockito.mock(ConsumerOffsetManager.class); + Mockito.when(brokerController.getBrokerConfig()).thenReturn(new BrokerConfig()); + Mockito.when(brokerController.getConsumerOffsetManager()).thenReturn(consumerOffsetManager); + Mockito.when(consumerLockService.tryLock(groupId, topicId)).thenReturn(true); + + PopConsumerCache consumerCache = + new PopConsumerCache(brokerController, consumerKVStore, consumerLockService, null); + consumerCache.commitOffset("CommitOffsetTest", groupId, topicId, queueId, 100L); + consumerCache.removeRecords(groupId, topicId, queueId); + + AtomicInteger estimateCacheSize = (AtomicInteger) FieldUtils.readField( + consumerCache, "estimateCacheSize", true); + estimateCacheSize.set(2); + consumerCache.start(); + Awaitility.await().until(() -> estimateCacheSize.get() == 0); + consumerCache.shutdown(); + } + + @Test + public void consumerCacheTest() { + BrokerController brokerController = Mockito.mock(BrokerController.class); + PopConsumerKVStore consumerKVStore = Mockito.mock(PopConsumerRocksdbStore.class); + PopConsumerLockService consumerLockService = Mockito.mock(PopConsumerLockService.class); + Mockito.when(brokerController.getBrokerConfig()).thenReturn(new BrokerConfig()); + + PopConsumerCache consumerCache = + new PopConsumerCache(brokerController, consumerKVStore, consumerLockService, null); + Assert.assertEquals(-1L, consumerCache.getMinOffsetInCache(groupId, topicId, queueId)); + Assert.assertEquals(0, consumerCache.getPopInFlightMessageCount(groupId, topicId, queueId)); + Assert.assertEquals(0, consumerCache.getCacheKeySize()); + + // write + for (int i = 0; i < 3; i++) { + PopConsumerRecord record = new PopConsumerRecord(2L, groupId, topicId, queueId, + 0, 20000, 100 + i, attemptId); + Assert.assertEquals(consumerCache.getKey(record), consumerCache.getKey(groupId, topicId, queueId)); + consumerCache.writeRecords(Collections.singletonList(record)); + } + Assert.assertEquals(100, consumerCache.getMinOffsetInCache(groupId, topicId, queueId)); + Assert.assertEquals(3, consumerCache.getPopInFlightMessageCount(groupId, topicId, queueId)); + Assert.assertEquals(1, consumerCache.getCacheKeySize()); + Assert.assertEquals(3, consumerCache.getCacheSize()); + Assert.assertFalse(consumerCache.isCacheFull()); + + // delete + PopConsumerRecord record = new PopConsumerRecord(2L, groupId, topicId, queueId, + 0, 20000, 100, attemptId); + Assert.assertEquals(0, consumerCache.deleteRecords(Collections.singletonList(record)).size()); + Assert.assertEquals(101, consumerCache.getMinOffsetInCache(groupId, topicId, queueId)); + Assert.assertEquals(2, consumerCache.getPopInFlightMessageCount(groupId, topicId, queueId)); + Assert.assertEquals(2, consumerCache.getCacheSize()); + + record = new PopConsumerRecord(2L, groupId, topicId, queueId, + 0, 20000, 104, attemptId); + Assert.assertEquals(1, consumerCache.deleteRecords(Collections.singletonList(record)).size()); + Assert.assertEquals(101, consumerCache.getMinOffsetInCache(groupId, topicId, queueId)); + Assert.assertEquals(2, consumerCache.getPopInFlightMessageCount(groupId, topicId, queueId)); + + // clean expired records + Queue consumerRecordList = new LinkedBlockingQueue<>(); + consumerCache.cleanupRecords(consumerRecordList::add); + Assert.assertEquals(2, consumerRecordList.size()); + + // clean all + Mockito.when(consumerLockService.isLockTimeout(any(), any())).thenReturn(true); + consumerRecordList.clear(); + consumerCache.cleanupRecords(consumerRecordList::add); + Assert.assertEquals(0, consumerRecordList.size()); + } +} \ No newline at end of file diff --git a/broker/src/test/java/org/apache/rocketmq/broker/pop/PopConsumerContextTest.java b/broker/src/test/java/org/apache/rocketmq/broker/pop/PopConsumerContextTest.java new file mode 100644 index 0000000..6f00942 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/pop/PopConsumerContextTest.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.broker.pop; + +import org.apache.rocketmq.common.constant.ConsumeInitMode; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.GetMessageStatus; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +public class PopConsumerContextTest { + + @Test + public void consumerContextTest() { + long popTime = System.currentTimeMillis(); + PopConsumerContext context = new PopConsumerContext("127.0.0.1:6789", + popTime, 20_000, "GroupId", true, ConsumeInitMode.MIN, "attemptId"); + + Assert.assertFalse(context.isFound()); + Assert.assertEquals("127.0.0.1:6789", context.getClientHost()); + Assert.assertEquals(popTime, context.getPopTime()); + Assert.assertEquals(20_000, context.getInvisibleTime()); + Assert.assertEquals("GroupId", context.getGroupId()); + Assert.assertTrue(context.isFifo()); + Assert.assertEquals("attemptId", context.getAttemptId()); + Assert.assertEquals(0, context.getRestCount()); + + GetMessageResult getMessageResult = new GetMessageResult(); + getMessageResult.setStatus(GetMessageStatus.FOUND); + getMessageResult.setMinOffset(10L); + getMessageResult.setMaxOffset(20L); + getMessageResult.setNextBeginOffset(15L); + getMessageResult.addMessage(Mockito.mock(SelectMappedBufferResult.class), 10); + getMessageResult.addMessage(Mockito.mock(SelectMappedBufferResult.class), 12); + getMessageResult.addMessage(Mockito.mock(SelectMappedBufferResult.class), 13); + + context.addGetMessageResult(getMessageResult, + "TopicId", 3, PopConsumerRecord.RetryType.NORMAL_TOPIC, 1); + + Assert.assertEquals(3, context.getMessageCount()); + Assert.assertEquals( + getMessageResult.getMaxOffset() - getMessageResult.getNextBeginOffset(), context.getRestCount()); + + // check header + Assert.assertNotNull(context.toString()); + Assert.assertEquals("0 3 1", context.getStartOffsetInfo()); + Assert.assertEquals("0 3 10,12,13", context.getMsgOffsetInfo()); + Assert.assertNotNull(context.getOrderCountInfoBuilder()); + Assert.assertEquals("", context.getOrderCountInfo()); + + Assert.assertEquals(1, context.getGetMessageResultList().size()); + Assert.assertEquals(3, context.getPopConsumerRecordList().size()); + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/pop/PopConsumerLockServiceTest.java b/broker/src/test/java/org/apache/rocketmq/broker/pop/PopConsumerLockServiceTest.java new file mode 100644 index 0000000..b5af2f3 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/pop/PopConsumerLockServiceTest.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.broker.pop; + +import java.lang.reflect.Field; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.common.PopAckConstants; +import org.junit.Assert; +import org.junit.Test; + +public class PopConsumerLockServiceTest { + + @Test + @SuppressWarnings("unchecked") + public void consumerLockTest() throws NoSuchFieldException, IllegalAccessException { + String groupId = "groupId"; + String topicId = "topicId"; + + PopConsumerLockService lockService = + new PopConsumerLockService(TimeUnit.MINUTES.toMillis(2)); + + Assert.assertTrue(lockService.tryLock(groupId, topicId)); + Assert.assertFalse(lockService.tryLock(groupId, topicId)); + lockService.unlock(groupId, topicId); + + Assert.assertTrue(lockService.tryLock(groupId, topicId)); + Assert.assertFalse(lockService.tryLock(groupId, topicId)); + Assert.assertFalse(lockService.isLockTimeout(groupId, topicId)); + lockService.removeTimeout(); + + // set expired + Field field = PopConsumerLockService.class.getDeclaredField("lockTable"); + field.setAccessible(true); + Map table = + (Map) field.get(lockService); + + Field lockTime = PopConsumerLockService.TimedLock.class.getDeclaredField("lockTime"); + lockTime.setAccessible(true); + lockTime.set(table.get(groupId + PopAckConstants.SPLIT + topicId), + System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(3)); + lockService.removeTimeout(); + + Assert.assertEquals(0, table.size()); + } +} \ No newline at end of file diff --git a/broker/src/test/java/org/apache/rocketmq/broker/pop/PopConsumerRecordTest.java b/broker/src/test/java/org/apache/rocketmq/broker/pop/PopConsumerRecordTest.java new file mode 100644 index 0000000..24a79b3 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/pop/PopConsumerRecordTest.java @@ -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. + */ +package org.apache.rocketmq.broker.pop; + +import java.util.UUID; +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.junit.Assert; +import org.junit.Test; + +public class PopConsumerRecordTest { + + private static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); + + @Test + public void retryCodeTest() { + Assert.assertEquals("NORMAL_TOPIC code should be 0", + 0, PopConsumerRecord.RetryType.NORMAL_TOPIC.getCode()); + Assert.assertEquals("RETRY_TOPIC code should be 1", + 1, PopConsumerRecord.RetryType.RETRY_TOPIC_V1.getCode()); + Assert.assertEquals("RETRY_TOPIC_V2 code should be 2", + 2, PopConsumerRecord.RetryType.RETRY_TOPIC_V2.getCode()); + } + + @Test + public void deliveryRecordSerializeTest() { + PopConsumerRecord consumerRecord = new PopConsumerRecord(); + consumerRecord.setPopTime(System.currentTimeMillis()); + consumerRecord.setGroupId("GroupId"); + consumerRecord.setTopicId("TopicId"); + consumerRecord.setQueueId(3); + consumerRecord.setRetryFlag(PopConsumerRecord.RetryType.RETRY_TOPIC_V1.getCode()); + consumerRecord.setInvisibleTime(20); + consumerRecord.setOffset(100); + consumerRecord.setAttemptTimes(2); + consumerRecord.setAttemptId(UUID.randomUUID().toString().toUpperCase()); + + Assert.assertTrue(consumerRecord.isRetry()); + Assert.assertEquals(consumerRecord.getPopTime() + consumerRecord.getInvisibleTime(), + consumerRecord.getVisibilityTimeout()); + Assert.assertEquals(8 + "GroupId".length() + 1 + "TopicId".length() + 1 + 4 + 1 + 8, + consumerRecord.getKeyBytes().length); + log.info("ConsumerRecord={}", consumerRecord.toString()); + + PopConsumerRecord decodeRecord = PopConsumerRecord.decode(consumerRecord.getValueBytes()); + PopConsumerRecord consumerRecord2 = new PopConsumerRecord(consumerRecord.getPopTime(), + consumerRecord.getGroupId(), consumerRecord.getTopicId(), consumerRecord.getQueueId(), + consumerRecord.getRetryFlag(), consumerRecord.getInvisibleTime(), + consumerRecord.getOffset(), consumerRecord.getAttemptId()); + Assert.assertEquals(decodeRecord.getPopTime(), consumerRecord2.getPopTime()); + Assert.assertEquals(decodeRecord.getGroupId(), consumerRecord2.getGroupId()); + Assert.assertEquals(decodeRecord.getTopicId(), consumerRecord2.getTopicId()); + Assert.assertEquals(decodeRecord.getQueueId(), consumerRecord2.getQueueId()); + Assert.assertEquals(decodeRecord.getRetryFlag(), consumerRecord2.getRetryFlag()); + Assert.assertEquals(decodeRecord.getInvisibleTime(), consumerRecord2.getInvisibleTime()); + Assert.assertEquals(decodeRecord.getOffset(), consumerRecord2.getOffset()); + Assert.assertEquals(0, consumerRecord2.getAttemptTimes()); + Assert.assertEquals(decodeRecord.getAttemptId(), consumerRecord2.getAttemptId()); + } +} \ No newline at end of file diff --git a/broker/src/test/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStoreTest.java b/broker/src/test/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStoreTest.java new file mode 100644 index 0000000..3c2b190 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStoreTest.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.broker.pop; + +import com.google.common.base.Stopwatch; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.apache.commons.io.FileUtils; +import org.apache.rocketmq.common.config.AbstractRocksDBStorage; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtil; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; +import org.rocksdb.RocksDB; +import org.rocksdb.RocksIterator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PopConsumerRocksdbStoreTest { + + private static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); + private static final String CONSUMER_STORE_PATH = "consumer_rocksdb"; + + public static String getRandomStorePath() { + return Paths.get(System.getProperty("user.home"), "store_test", CONSUMER_STORE_PATH, + UUID.randomUUID().toString().replace("-", "").toUpperCase().substring(0, 16)).toString(); + } + + public static void deleteStoreDirectory(String storePath) { + try { + FileUtils.deleteDirectory(new File(storePath)); + } catch (IOException e) { + log.error("Delete store directory failed, filePath: {}", storePath, e); + } + } + + public static PopConsumerRecord getConsumerRecord() { + return new PopConsumerRecord(1L, "GroupTest", "TopicTest", 2, + PopConsumerRecord.RetryType.NORMAL_TOPIC.getCode(), TimeUnit.SECONDS.toMillis(20), 100L, "AttemptId"); + } + + @Test + public void rocksdbStoreWriteDeleteTest() { + String filePath = getRandomStorePath(); + PopConsumerKVStore consumerStore = new PopConsumerRocksdbStore(filePath); + Assert.assertEquals(filePath, consumerStore.getFilePath()); + + consumerStore.start(); + consumerStore.writeRecords(IntStream.range(0, 3).boxed() + .flatMap(i -> + IntStream.range(0, 5).mapToObj(j -> { + PopConsumerRecord consumerRecord = getConsumerRecord(); + consumerRecord.setPopTime(j); + consumerRecord.setQueueId(i); + consumerRecord.setOffset(100L + j); + return consumerRecord; + }) + ) + .collect(Collectors.toList())); + consumerStore.deleteRecords(IntStream.range(0, 2).boxed() + .flatMap(i -> + IntStream.range(0, 5).mapToObj(j -> { + PopConsumerRecord consumerRecord = getConsumerRecord(); + consumerRecord.setPopTime(j); + consumerRecord.setQueueId(i); + consumerRecord.setOffset(100L + j); + return consumerRecord; + }) + ) + .collect(Collectors.toList())); + + List consumerRecords = + consumerStore.scanExpiredRecords(0, 20002, 2); + Assert.assertEquals(2, consumerRecords.size()); + consumerStore.deleteRecords(consumerRecords); + + consumerRecords = consumerStore.scanExpiredRecords(0, 20003, 2); + Assert.assertEquals(1, consumerRecords.size()); + consumerStore.deleteRecords(consumerRecords); + + consumerRecords = consumerStore.scanExpiredRecords(0, 20005, 3); + Assert.assertEquals(2, consumerRecords.size()); + + consumerStore.shutdown(); + deleteStoreDirectory(filePath); + } + + private long getDirectorySizeRecursive(File directory) { + long size = 0; + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isFile()) { + size += file.length(); + } else if (file.isDirectory()) { + size += getDirectorySizeRecursive(file); + } + } + } + return size; + } + + @Test + @Ignore + @SuppressWarnings("ConstantValue") + public void tombstoneDeletionTest() throws IllegalAccessException, NoSuchFieldException { + PopConsumerRocksdbStore rocksdbStore = new PopConsumerRocksdbStore(getRandomStorePath()); + rocksdbStore.start(); + + int iterCount = 1000 * 1000; + boolean useSeekFirstDelete = false; + Field dbField = AbstractRocksDBStorage.class.getDeclaredField("db"); + dbField.setAccessible(true); + RocksDB rocksDB = (RocksDB) dbField.get(rocksdbStore); + + long currentTime = 0L; + Stopwatch stopwatch = Stopwatch.createStarted(); + for (int i = 0; i < iterCount; i++) { + List records = new ArrayList<>(); + for (int j = 0; j < 1000; j++) { + PopConsumerRecord record = getConsumerRecord(); + record.setPopTime((long) i * iterCount + j); + record.setGroupId("GroupTest"); + record.setTopicId("TopicTest"); + record.setQueueId(i % 10); + record.setRetryFlag(0); + record.setInvisibleTime(TimeUnit.SECONDS.toMillis(30)); + record.setOffset(i); + records.add(record); + } + rocksdbStore.writeRecords(records); + + long start = stopwatch.elapsed(TimeUnit.MILLISECONDS); + List deleteList = new ArrayList<>(); + if (useSeekFirstDelete) { + try (RocksIterator iterator = rocksDB.newIterator(rocksdbStore.columnFamilyHandle)) { + iterator.seekToFirst(); + if (i % 10 == 0) { + long fileSize = getDirectorySizeRecursive(new File(rocksdbStore.getFilePath())); + log.info("DirectorySize={}, Cost={}ms", + MessageStoreUtil.toHumanReadable(fileSize), stopwatch.elapsed(TimeUnit.MILLISECONDS) - start); + } + while (iterator.isValid() && deleteList.size() < 1024) { + deleteList.add(PopConsumerRecord.decode(iterator.value())); + iterator.next(); + } + } + } else { + long upper = System.currentTimeMillis(); + deleteList = rocksdbStore.scanExpiredRecords(currentTime, upper, 800); + if (!deleteList.isEmpty()) { + currentTime = deleteList.get(deleteList.size() - 1).getVisibilityTimeout(); + } + long scanCost = stopwatch.elapsed(TimeUnit.MILLISECONDS) - start; + if (i % 100 == 0) { + long fileSize = getDirectorySizeRecursive(new File(rocksdbStore.getFilePath())); + long seekTime = stopwatch.elapsed(TimeUnit.MILLISECONDS); + try (RocksIterator iterator = rocksDB.newIterator(rocksdbStore.columnFamilyHandle)) { + iterator.seekToFirst(); + } + log.info("DirectorySize={}, Cost={}ms, SeekFirstCost={}ms", MessageStoreUtil.toHumanReadable(fileSize), + scanCost, stopwatch.elapsed(TimeUnit.MILLISECONDS) - seekTime); + } + } + rocksdbStore.deleteRecords(deleteList); + } + rocksdbStore.shutdown(); + deleteStoreDirectory(rocksdbStore.getFilePath()); + } +} \ No newline at end of file diff --git a/broker/src/test/java/org/apache/rocketmq/broker/pop/PopConsumerServiceTest.java b/broker/src/test/java/org/apache/rocketmq/broker/pop/PopConsumerServiceTest.java new file mode 100644 index 0000000..9c23a86 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/pop/PopConsumerServiceTest.java @@ -0,0 +1,454 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.pop; + +import java.io.File; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.commons.lang3.tuple.Triple; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.failover.EscapeBridge; +import org.apache.rocketmq.broker.longpolling.PopLongPollingService; +import org.apache.rocketmq.broker.offset.ConsumerOffsetManager; +import org.apache.rocketmq.broker.offset.ConsumerOrderInfoManager; +import org.apache.rocketmq.broker.processor.PopMessageProcessor; +import org.apache.rocketmq.broker.topic.TopicConfigManager; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.constant.ConsumeInitMode; +import org.apache.rocketmq.common.constant.PermName; +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.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; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.exception.ConsumeQueueException; +import org.apache.rocketmq.store.stats.BrokerStatsManager; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; + +public class PopConsumerServiceTest { + + private final String clientHost = "127.0.0.1:8888"; + private final String groupId = "groupId"; + private final String topicId = "topicId"; + private final int queueId = 2; + private final String attemptId = UUID.randomUUID().toString().toUpperCase(); + private final String filePath = PopConsumerRocksdbStoreTest.getRandomStorePath(); + + private BrokerController brokerController; + private PopConsumerService consumerService; + + @Before + public void init() throws IOException { + BrokerConfig brokerConfig = new BrokerConfig(); + brokerConfig.setEnablePopLog(true); + brokerConfig.setEnablePopBufferMerge(true); + brokerConfig.setEnablePopMessageThreshold(true); + brokerConfig.setPopInflightMessageThreshold(100); + brokerConfig.setPopConsumerKVServiceLog(true); + brokerConfig.setEnableRetryTopicV2(true); + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setStorePathRootDir(filePath); + + TopicConfigManager topicConfigManager = Mockito.mock(TopicConfigManager.class); + ConsumerOffsetManager consumerOffsetManager = Mockito.mock(ConsumerOffsetManager.class); + PopMessageProcessor popMessageProcessor = Mockito.mock(PopMessageProcessor.class); + PopLongPollingService popLongPollingService = Mockito.mock(PopLongPollingService.class); + ConsumerOrderInfoManager consumerOrderInfoManager = Mockito.mock(ConsumerOrderInfoManager.class); + + brokerController = Mockito.mock(BrokerController.class); + Mockito.when(brokerController.getBrokerConfig()).thenReturn(brokerConfig); + Mockito.when(brokerController.getTopicConfigManager()).thenReturn(topicConfigManager); + Mockito.when(brokerController.getMessageStoreConfig()).thenReturn(messageStoreConfig); + Mockito.when(brokerController.getConsumerOffsetManager()).thenReturn(consumerOffsetManager); + Mockito.when(brokerController.getPopMessageProcessor()).thenReturn(popMessageProcessor); + Mockito.when(popMessageProcessor.getPopLongPollingService()).thenReturn(popLongPollingService); + Mockito.when(brokerController.getConsumerOrderInfoManager()).thenReturn(consumerOrderInfoManager); + + consumerService = new PopConsumerService(brokerController); + } + + @After + public void shutdown() throws IOException { + FileUtils.deleteDirectory(new File(filePath)); + } + + public PopConsumerRecord getConsumerTestRecord() { + PopConsumerRecord popConsumerRecord = new PopConsumerRecord(); + popConsumerRecord.setPopTime(System.currentTimeMillis()); + popConsumerRecord.setGroupId(groupId); + popConsumerRecord.setTopicId(topicId); + popConsumerRecord.setQueueId(queueId); + popConsumerRecord.setRetryFlag(PopConsumerRecord.RetryType.NORMAL_TOPIC.getCode()); + popConsumerRecord.setAttemptTimes(0); + popConsumerRecord.setInvisibleTime(TimeUnit.SECONDS.toMillis(20)); + popConsumerRecord.setAttemptId(UUID.randomUUID().toString().toUpperCase()); + return popConsumerRecord; + } + + @Test + public void isPopShouldStopTest() throws IllegalAccessException { + Assert.assertFalse(consumerService.isPopShouldStop(groupId, topicId, queueId)); + PopConsumerCache consumerCache = (PopConsumerCache) FieldUtils.readField( + consumerService, "popConsumerCache", true); + for (int i = 0; i < 100; i++) { + PopConsumerRecord record = getConsumerTestRecord(); + record.setOffset(i); + consumerCache.writeRecords(Collections.singletonList(record)); + } + Assert.assertTrue(consumerService.isPopShouldStop(groupId, topicId, queueId)); + } + + @Test + public void pendingFilterCountTest() throws ConsumeQueueException { + MessageStore messageStore = Mockito.mock(MessageStore.class); + Mockito.when(messageStore.getMaxOffsetInQueue(topicId, queueId)).thenReturn(100L); + Mockito.when(brokerController.getMessageStore()).thenReturn(messageStore); + ConsumerOffsetManager consumerOffsetManager = brokerController.getConsumerOffsetManager(); + Mockito.when(consumerOffsetManager.queryOffset(groupId, topicId, queueId)).thenReturn(20L); + Assert.assertEquals(consumerService.getPendingFilterCount(groupId, topicId, queueId), 80L); + } + + private MessageExt getMessageExt() { + MessageExt messageExt = new MessageExt(); + messageExt.setTopic(topicId); + messageExt.setQueueId(queueId); + messageExt.setBody(new byte[128]); + messageExt.setBornHost(new InetSocketAddress("127.0.0.1", 8080)); + messageExt.setStoreHost(new InetSocketAddress("127.0.0.1", 8080)); + messageExt.putUserProperty("Key", "Value"); + return messageExt; + } + + @Test + public void recodeRetryMessageTest() throws Exception { + GetMessageResult getMessageResult = new GetMessageResult(); + getMessageResult.setStatus(GetMessageStatus.FOUND); + + // result is empty + SelectMappedBufferResult bufferResult = new SelectMappedBufferResult( + 0, ByteBuffer.allocate(10), 10, null); + getMessageResult.addMessage(bufferResult); + getMessageResult.getMessageMapedList().clear(); + GetMessageResult result = consumerService.recodeRetryMessage( + getMessageResult, topicId, 0, 100, 200); + Assert.assertEquals(0, result.getMessageMapedList().size()); + + ByteBuffer buffer = ByteBuffer.wrap( + MessageDecoder.encode(getMessageExt(), false)); + getMessageResult = new GetMessageResult(); + getMessageResult.setStatus(GetMessageStatus.FOUND); + getMessageResult.addMessage(new SelectMappedBufferResult( + 0, buffer, buffer.remaining(), null)); + result = consumerService.recodeRetryMessage( + getMessageResult, topicId, 0, 100, 200); + Assert.assertNotNull(result); + Assert.assertEquals(1, result.getMessageMapedList().size()); + } + + @Test + public void addGetMessageResultTest() { + PopConsumerContext context = new PopConsumerContext( + clientHost, System.currentTimeMillis(), 20000, groupId, false, ConsumeInitMode.MIN, attemptId); + GetMessageResult result = new GetMessageResult(); + result.setStatus(GetMessageStatus.FOUND); + result.getMessageQueueOffset().add(100L); + consumerService.handleGetMessageResult( + context, result, topicId, queueId, PopConsumerRecord.RetryType.NORMAL_TOPIC, 100); + Assert.assertEquals(1, context.getGetMessageResultList().size()); + } + + @Test + public void getMessageAsyncTest() throws Exception { + MessageStore messageStore = Mockito.mock(MessageStore.class); + Mockito.when(brokerController.getMessageStore()).thenReturn(messageStore); + Mockito.when(messageStore.getMessageAsync(groupId, topicId, queueId, 0, 10, null)) + .thenReturn(CompletableFuture.completedFuture(null)); + GetMessageResult getMessageResult = consumerService.getMessageAsync( + "127.0.0.1:8888", groupId, topicId, queueId, 0, 10, null).join(); + Assert.assertNull(getMessageResult); + + // success when first get message + GetMessageResult firstGetMessageResult = new GetMessageResult(); + firstGetMessageResult.setStatus(GetMessageStatus.FOUND); + Mockito.when(messageStore.getMessageAsync(groupId, topicId, queueId, 0, 10, null)) + .thenReturn(CompletableFuture.completedFuture(firstGetMessageResult)); + getMessageResult = consumerService.getMessageAsync( + "127.0.0.1:8888", groupId, topicId, queueId, 0, 10, null).join(); + Assert.assertEquals(GetMessageStatus.FOUND, getMessageResult.getStatus()); + + // reset offset from server + firstGetMessageResult.setStatus(GetMessageStatus.OFFSET_FOUND_NULL); + firstGetMessageResult.setNextBeginOffset(25); + GetMessageResult resetGetMessageResult = new GetMessageResult(); + resetGetMessageResult.setStatus(GetMessageStatus.FOUND); + Mockito.when(messageStore.getMessageAsync(groupId, topicId, queueId, 25, 10, null)) + .thenReturn(CompletableFuture.completedFuture(resetGetMessageResult)); + getMessageResult = consumerService.getMessageAsync( + "127.0.0.1:8888", groupId, topicId, queueId, 0, 10, null).join(); + Assert.assertEquals(GetMessageStatus.FOUND, getMessageResult.getStatus()); + + // fifo block + PopConsumerContext context = new PopConsumerContext( + clientHost, System.currentTimeMillis(), 20000, groupId, false, ConsumeInitMode.MIN, attemptId); + consumerService.setFifoBlocked(context, groupId, topicId, queueId, Collections.singletonList(100L)); + Mockito.when(brokerController.getConsumerOrderInfoManager() + .checkBlock(anyString(), anyString(), anyString(), anyInt(), anyLong())).thenReturn(true); + Assert.assertTrue(consumerService.isFifoBlocked(context, groupId, topicId, queueId)); + + // get message async normal + CompletableFuture future = CompletableFuture.completedFuture(context); + Assert.assertEquals(0L, consumerService.getMessageAsync(future, clientHost, groupId, topicId, queueId, + 10, null, PopConsumerRecord.RetryType.NORMAL_TOPIC).join().getRestCount()); + + // get message result full, no need get again + for (int i = 0; i < 10; i++) { + ByteBuffer buffer = ByteBuffer.wrap(MessageDecoder.encode(getMessageExt(), false)); + getMessageResult.addMessage(new SelectMappedBufferResult( + 0, buffer, buffer.remaining(), null), i); + } + context.addGetMessageResult(getMessageResult, topicId, queueId, PopConsumerRecord.RetryType.NORMAL_TOPIC, 0); + + Mockito.when(brokerController.getMessageStore().getMaxOffsetInQueue(topicId, queueId)).thenReturn(100L); + Mockito.when(brokerController.getConsumerOffsetManager().queryOffset(groupId, topicId, queueId)).thenReturn(0L); + Assert.assertEquals(100L, consumerService.getMessageAsync(future, clientHost, groupId, topicId, queueId, + 10, null, PopConsumerRecord.RetryType.NORMAL_TOPIC).join().getRestCount()); + + // fifo block test + context = new PopConsumerContext( + clientHost, System.currentTimeMillis(), 20000, groupId, true, ConsumeInitMode.MIN, attemptId); + future = CompletableFuture.completedFuture(context); + Assert.assertEquals(0L, consumerService.getMessageAsync(future, clientHost, groupId, topicId, queueId, + 10, null, PopConsumerRecord.RetryType.NORMAL_TOPIC).join().getRestCount()); + } + + @Test + public void popAsyncTest() { + PopConsumerService consumerServiceSpy = Mockito.spy(consumerService); + TopicConfigManager topicConfigManager = Mockito.mock(TopicConfigManager.class); + Mockito.when(topicConfigManager.selectTopicConfig(topicId)).thenReturn(new TopicConfig( + topicId, 2, 2, PermName.PERM_READ | PermName.PERM_WRITE, 0)); + Mockito.when(brokerController.getTopicConfigManager()).thenReturn(topicConfigManager); + + String[] retryTopic = new String[] { + KeyBuilder.buildPopRetryTopicV1(topicId, groupId), + KeyBuilder.buildPopRetryTopicV2(topicId, groupId) + }; + + for (String retry : retryTopic) { + GetMessageResult getMessageResult = new GetMessageResult(); + getMessageResult.setStatus(GetMessageStatus.NO_MATCHED_MESSAGE); + getMessageResult.setMinOffset(0L); + getMessageResult.setMaxOffset(1L); + getMessageResult.setNextBeginOffset(1L); + Mockito.doReturn(CompletableFuture.completedFuture(getMessageResult)) + .when(consumerServiceSpy).getMessageAsync(clientHost, groupId, retry, 0, 0, 10, null); + Mockito.doReturn(CompletableFuture.completedFuture(getMessageResult)) + .when(consumerServiceSpy).getMessageAsync(clientHost, groupId, retry, 0, 0, 8, null); + } + + for (int i = -1; i < 2; i++) { + GetMessageResult getMessageResult = new GetMessageResult(); + getMessageResult.setStatus(GetMessageStatus.FOUND); + getMessageResult.setMinOffset(0L); + getMessageResult.setMaxOffset(1L); + getMessageResult.setNextBeginOffset(1L); + getMessageResult.addMessage(Mockito.mock(SelectMappedBufferResult.class), 1L); + + Mockito.doReturn(CompletableFuture.completedFuture(getMessageResult)) + .when(consumerServiceSpy).getMessageAsync(clientHost, groupId, topicId, i, 0, 8, null); + Mockito.doReturn(CompletableFuture.completedFuture(getMessageResult)) + .when(consumerServiceSpy).getMessageAsync(clientHost, groupId, topicId, i, 0, 9, null); + Mockito.doReturn(CompletableFuture.completedFuture(getMessageResult)) + .when(consumerServiceSpy).getMessageAsync(clientHost, groupId, topicId, i, 0, 10, null); + } + + // pop broker + consumerServiceSpy.popAsync(clientHost, System.currentTimeMillis(), + 20000, groupId, topicId, -1, 10, false, attemptId, ConsumeInitMode.MIN, null).join(); + } + + @Test + public void ackAsyncTest() { + long current = System.currentTimeMillis(); + consumerService.getPopConsumerStore().start(); + consumerService.ackAsync( + current, 10, groupId, topicId, queueId, 100).join(); + consumerService.changeInvisibilityDuration(current, 10, + current + 100, 10, groupId, topicId, queueId, 100); + consumerService.shutdown(); + } + + @Test + public void reviveRetryTest() { + Mockito.when(brokerController.getTopicConfigManager().selectTopicConfig(topicId)).thenReturn(null); + Mockito.when(brokerController.getConsumerOffsetManager().queryOffset(groupId, topicId, 0)).thenReturn(-1L); + + consumerService.createRetryTopicIfNeeded(groupId, topicId); + consumerService.clearCache(groupId, topicId, queueId); + MessageExt messageExt = new MessageExt(); + messageExt.setBody("body".getBytes()); + messageExt.setBornTimestamp(System.currentTimeMillis()); + messageExt.setFlag(0); + messageExt.setSysFlag(0); + messageExt.setReconsumeTimes(1); + messageExt.putUserProperty("key", "value"); + + PopConsumerRecord record = new PopConsumerRecord(); + record.setTopicId("topic"); + record.setGroupId("group"); + Mockito.when(brokerController.getBrokerStatsManager()).thenReturn(Mockito.mock(BrokerStatsManager.class)); + Mockito.when(brokerController.getEscapeBridge()).thenReturn(Mockito.mock(EscapeBridge.class)); + Mockito.when(brokerController.getEscapeBridge().putMessageToSpecificQueue(any(MessageExtBrokerInner.class))) + .thenReturn(new PutMessageResult( + PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK))); + + PopConsumerService consumerServiceSpy = Mockito.spy(consumerService); + Mockito.doNothing().when(consumerServiceSpy).createRetryTopicIfNeeded(any(), any()); + Assert.assertTrue(consumerServiceSpy.reviveRetry(record, messageExt)); + + // write message error + Mockito.when(brokerController.getEscapeBridge().putMessageToSpecificQueue(any(MessageExtBrokerInner.class))) + .thenReturn(new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, + new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR))); + Assert.assertFalse(consumerServiceSpy.reviveRetry(record, messageExt)); + + // revive backoff + consumerService.getPopConsumerStore().start(); + List consumerRecordList = IntStream.range(0, 3) + .mapToObj(i -> { + PopConsumerRecord temp = new PopConsumerRecord(); + temp.setPopTime(0); + temp.setInvisibleTime(20 * 1000); + temp.setTopicId("topic"); + temp.setGroupId("group"); + temp.setQueueId(2); + temp.setOffset(i); + return temp; + }) + .collect(Collectors.toList()); + consumerService.getPopConsumerStore().writeRecords(consumerRecordList); + + Mockito.doReturn(CompletableFuture.completedFuture(null)) + .when(consumerServiceSpy).getMessageAsync(any(PopConsumerRecord.class)); + consumerServiceSpy.revive(new AtomicLong(20 * 1000), 1); + + Mockito.doReturn(CompletableFuture.completedFuture( + Triple.of(null, "GetMessageResult is null", false))) + .when(consumerServiceSpy).getMessageAsync(any(PopConsumerRecord.class)); + consumerServiceSpy.revive(new AtomicLong(20 * 1000), 1); + + Mockito.doReturn(CompletableFuture.completedFuture( + Triple.of(Mockito.mock(MessageExt.class), null, false))) + .when(consumerServiceSpy).getMessageAsync(any(PopConsumerRecord.class)); + consumerServiceSpy.revive(new AtomicLong(20 * 1000), 1); + consumerService.shutdown(); + } + + @Test + public void reviveBackoffRetryTest() { + Mockito.when(brokerController.getEscapeBridge()).thenReturn(Mockito.mock(EscapeBridge.class)); + PopConsumerService consumerServiceSpy = Mockito.spy(consumerService); + + consumerService.getPopConsumerStore().start(); + + long popTime = 1000000000L; + long invisibleTime = 60 * 1000L; + PopConsumerRecord record = new PopConsumerRecord(); + record.setPopTime(popTime); + record.setInvisibleTime(invisibleTime); + record.setTopicId("topic"); + record.setGroupId("group"); + record.setQueueId(0); + record.setOffset(0); + consumerService.getPopConsumerStore().writeRecords(Collections.singletonList(record)); + + Mockito.doReturn(CompletableFuture.completedFuture(Triple.of(Mockito.mock(MessageExt.class), "", false))) + .when(consumerServiceSpy).getMessageAsync(any(PopConsumerRecord.class)); + Mockito.when(brokerController.getEscapeBridge().putMessageToSpecificQueue(any(MessageExtBrokerInner.class))).thenReturn( + new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR)) + ); + + long visibleTimestamp = popTime + invisibleTime; + + // revive fails + Assert.assertEquals(1, consumerServiceSpy.revive(new AtomicLong(visibleTimestamp), 1)); + // should be invisible now + Assert.assertEquals(0, consumerService.getPopConsumerStore().scanExpiredRecords(0, visibleTimestamp, 1).size()); + // will be visible again in 10 seconds + Assert.assertEquals(1, consumerService.getPopConsumerStore().scanExpiredRecords(visibleTimestamp, System.currentTimeMillis() + visibleTimestamp + 10 * 1000, 1).size()); + + consumerService.shutdown(); + } + + @Test + public void transferToFsStoreTest() { + Assert.assertNotNull(consumerService.getServiceName()); + List consumerRecordList = IntStream.range(0, 3) + .mapToObj(i -> { + PopConsumerRecord temp = new PopConsumerRecord(); + temp.setPopTime(0); + temp.setInvisibleTime(20 * 1000); + temp.setTopicId("topic"); + temp.setGroupId("group"); + temp.setQueueId(2); + temp.setOffset(i); + return temp; + }) + .collect(Collectors.toList()); + + Mockito.when(brokerController.getPopMessageProcessor().buildCkMsg(any(), anyInt())) + .thenReturn(new MessageExtBrokerInner()); + Mockito.when(brokerController.getMessageStore()).thenReturn(Mockito.mock(MessageStore.class)); + Mockito.when(brokerController.getMessageStore().asyncPutMessage(any())) + .thenReturn(CompletableFuture.completedFuture( + new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK)))); + + consumerService.start(); + consumerService.getPopConsumerStore().writeRecords(consumerRecordList); + consumerService.transferToFsStore(); + consumerService.shutdown(); + } +} \ No newline at end of file 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 0000000..757b01b --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/AckMessageProcessorTest.java @@ -0,0 +1,362 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.apache.rocketmq.store.exception.ConsumeQueueException; +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, ConsumeQueueException { + 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 0000000..a6bcca9 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessorTest.java @@ -0,0 +1,1437 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.rocketmq.auth.authentication.enums.UserType; +import org.apache.rocketmq.auth.authentication.manager.AuthenticationMetadataManager; +import org.apache.rocketmq.auth.authentication.model.Subject; +import org.apache.rocketmq.auth.authentication.model.User; +import org.apache.rocketmq.auth.authorization.enums.Decision; +import org.apache.rocketmq.auth.authorization.manager.AuthorizationMetadataManager; +import org.apache.rocketmq.auth.authorization.model.Acl; +import org.apache.rocketmq.auth.authorization.model.Environment; +import org.apache.rocketmq.auth.authorization.model.Resource; +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.client.net.Broker2Client; +import org.apache.rocketmq.broker.offset.ConsumerOffsetManager; +import org.apache.rocketmq.broker.schedule.ScheduleMessageService; +import org.apache.rocketmq.broker.config.v1.RocksDBSubscriptionGroupManager; +import org.apache.rocketmq.broker.config.v1.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.MQVersion; +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.action.Action; +import org.apache.rocketmq.common.attribute.AttributeParser; +import org.apache.rocketmq.common.constant.FIleReadaheadMode; +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.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.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.AclInfo; +import org.apache.rocketmq.remoting.protocol.body.CreateTopicListRequestBody; +import org.apache.rocketmq.remoting.protocol.body.GroupList; +import org.apache.rocketmq.remoting.protocol.body.HARuntimeInfo; +import org.apache.rocketmq.remoting.protocol.body.LockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.body.QueryCorrectionOffsetBody; +import org.apache.rocketmq.remoting.protocol.body.UnlockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.body.UserInfo; +import org.apache.rocketmq.remoting.protocol.header.CreateAclRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.CreateTopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.CreateUserRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.DeleteAclRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.DeleteTopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.DeleteUserRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ExchangeHAInfoResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.GetAclRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetAllTopicConfigResponseHeader; +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.GetMaxOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMinOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetTopicConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetUserRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ListAclsRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ListUsersRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.NotifyMinBrokerIdChangeRequestHeader; +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.UpdateAclRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateUserRequestHeader; +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.CommitLog; +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.apache.rocketmq.store.timer.TimerCheckpoint; +import org.apache.rocketmq.store.timer.TimerMessageStore; +import org.apache.rocketmq.store.timer.TimerMetrics; +import org.apache.rocketmq.store.util.LibC; +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 java.io.IOException; +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +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.atomic.LongAdder; + +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.ArgumentMatchers.eq; +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(), null); + + @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; + @Mock + private AuthenticationMetadataManager authenticationMetadataManager; + @Mock + private AuthorizationMetadataManager authorizationMetadataManager; + + @Mock + private TimerMessageStore timerMessageStore; + + @Mock + private TimerMetrics timerMetrics; + + @Mock + private MessageStoreConfig messageStoreConfig; + + @Mock + private CommitLog commitLog; + + @Mock + private Broker2Client broker2Client; + + @Mock + private ClientChannelInfo clientChannelInfo; + + @Before + public void init() throws Exception { + brokerController.setMessageStore(messageStore); + brokerController.setAuthenticationMetadataManager(authenticationMetadataManager); + brokerController.setAuthorizationMetadataManager(authorizationMetadataManager); + Field field = BrokerController.class.getDeclaredField("broker2Client"); + field.setAccessible(true); + field.set(brokerController, broker2Client); + + //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); + when(this.brokerController.getMessageStore().getTimerMessageStore()).thenReturn(timerMessageStore); + when(this.timerMessageStore.getTimerMetrics()).thenReturn(timerMetrics); + } + + @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.INVALID_PARAMETER); + 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.INVALID_PARAMETER); + + topic = "TEST_CREATE_TOPIC"; + request = buildCreateTopicRequest(topic); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + // test deny MIXED topic type + brokerController.getBrokerConfig().setEnableMixedMessageType(false); + topic = "TEST_MIXED_TYPE"; + Map attributes = new HashMap<>(); + attributes.put("+message.type", "MIXED"); + request = buildCreateTopicRequest(topic, attributes); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.INVALID_PARAMETER); + // test allow MIXED topic type + brokerController.getBrokerConfig().setEnableMixedMessageType(true); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testUpdateAndCreateTopicList() throws RemotingCommandException { + List systemTopicList = new ArrayList<>(systemTopicSet); + RemotingCommand request = buildCreateTopicListRequest(systemTopicList); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.INVALID_PARAMETER); + assertThat(response.getRemark()).isEqualTo("The topic[" + systemTopicList.get(0) + "] is conflict with system topic."); + + List inValidTopicList = new ArrayList<>(); + inValidTopicList.add(""); + request = buildCreateTopicListRequest(inValidTopicList); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.INVALID_PARAMETER); + + List topicList = new ArrayList<>(); + topicList.add("TEST_CREATE_TOPIC"); + topicList.add("TEST_CREATE_TOPIC1"); + request = buildCreateTopicListRequest(topicList); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + //test no changes + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + // test deny MIXED topic type + brokerController.getBrokerConfig().setEnableMixedMessageType(false); + topicList.add("TEST_MIXED_TYPE"); + topicList.add("TEST_MIXED_TYPE1"); + Map attributes = new HashMap<>(); + attributes.put("+message.type", "MIXED"); + request = buildCreateTopicListRequest(topicList, attributes); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.INVALID_PARAMETER); + // test allow MIXED topic type + brokerController.getBrokerConfig().setEnableMixedMessageType(true); + 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.INVALID_PARAMETER); + 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."); + } + } + + @Test + public void testCreateUser() throws RemotingCommandException { + when(authenticationMetadataManager.createUser(any(User.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + + CreateUserRequestHeader createUserRequestHeader = new CreateUserRequestHeader(); + createUserRequestHeader.setUsername("abc"); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.AUTH_CREATE_USER, createUserRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + UserInfo userInfo = UserInfo.of("abc", "123", UserType.NORMAL.getName()); + request.setBody(JSON.toJSONBytes(userInfo)); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + when(authenticationMetadataManager.isSuperUser(eq("rocketmq"))).thenReturn(CompletableFuture.completedFuture(true)); + createUserRequestHeader = new CreateUserRequestHeader(); + createUserRequestHeader.setUsername("super"); + request = RemotingCommand.createRequestCommand(RequestCode.AUTH_CREATE_USER, createUserRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + userInfo = UserInfo.of("super", "123", UserType.SUPER.getName()); + request.setBody(JSON.toJSONBytes(userInfo)); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + when(authenticationMetadataManager.isSuperUser(eq("rocketmq"))).thenReturn(CompletableFuture.completedFuture(false)); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SYSTEM_ERROR); + } + + @Test + public void testUpdateUser() throws RemotingCommandException { + when(authenticationMetadataManager.updateUser(any(User.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + when(authenticationMetadataManager.getUser(eq("abc"))).thenReturn(CompletableFuture.completedFuture(User.of("abc", "123", UserType.NORMAL))); + when(authenticationMetadataManager.getUser(eq("super"))).thenReturn(CompletableFuture.completedFuture(User.of("super", "123", UserType.SUPER))); + + UpdateUserRequestHeader updateUserRequestHeader = new UpdateUserRequestHeader(); + updateUserRequestHeader.setUsername("abc"); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.AUTH_UPDATE_USER, updateUserRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + UserInfo userInfo = UserInfo.of("abc", "123", UserType.NORMAL.getName()); + request.setBody(JSON.toJSONBytes(userInfo)); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + when(authenticationMetadataManager.isSuperUser(eq("rocketmq"))).thenReturn(CompletableFuture.completedFuture(true)); + updateUserRequestHeader = new UpdateUserRequestHeader(); + updateUserRequestHeader.setUsername("super"); + request = RemotingCommand.createRequestCommand(RequestCode.AUTH_UPDATE_USER, updateUserRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + userInfo = UserInfo.of("super", "123", UserType.SUPER.getName()); + request.setBody(JSON.toJSONBytes(userInfo)); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + when(authenticationMetadataManager.isSuperUser(eq("rocketmq"))).thenReturn(CompletableFuture.completedFuture(false)); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SYSTEM_ERROR); + } + + @Test + public void testDeleteUser() throws RemotingCommandException { + when(authenticationMetadataManager.deleteUser(any(String.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + when(authenticationMetadataManager.getUser(eq("abc"))).thenReturn(CompletableFuture.completedFuture(User.of("abc", "123", UserType.NORMAL))); + when(authenticationMetadataManager.getUser(eq("super"))).thenReturn(CompletableFuture.completedFuture(User.of("super", "123", UserType.SUPER))); + + DeleteUserRequestHeader deleteUserRequestHeader = new DeleteUserRequestHeader(); + deleteUserRequestHeader.setUsername("abc"); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.AUTH_DELETE_USER, deleteUserRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + when(authenticationMetadataManager.isSuperUser(eq("rocketmq"))).thenReturn(CompletableFuture.completedFuture(true)); + deleteUserRequestHeader = new DeleteUserRequestHeader(); + deleteUserRequestHeader.setUsername("super"); + request = RemotingCommand.createRequestCommand(RequestCode.AUTH_DELETE_USER, deleteUserRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + when(authenticationMetadataManager.isSuperUser(eq("rocketmq"))).thenReturn(CompletableFuture.completedFuture(false)); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.NO_PERMISSION); + } + + @Test + public void testGetUser() throws RemotingCommandException { + when(authenticationMetadataManager.getUser(eq("abc"))).thenReturn(CompletableFuture.completedFuture(User.of("abc", "123", UserType.NORMAL))); + + GetUserRequestHeader getUserRequestHeader = new GetUserRequestHeader(); + getUserRequestHeader.setUsername("abc"); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.AUTH_GET_USER, getUserRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + UserInfo userInfo = JSON.parseObject(new String(response.getBody()), UserInfo.class); + assertThat(userInfo.getUsername()).isEqualTo("abc"); + assertThat(userInfo.getPassword()).isEqualTo("123"); + assertThat(userInfo.getUserType()).isEqualTo("Normal"); + } + + @Test + public void testListUser() throws RemotingCommandException { + when(authenticationMetadataManager.listUser(eq("abc"))).thenReturn(CompletableFuture.completedFuture(Arrays.asList(User.of("abc", "123", UserType.NORMAL)))); + + ListUsersRequestHeader listUserRequestHeader = new ListUsersRequestHeader(); + listUserRequestHeader.setFilter("abc"); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.AUTH_LIST_USER, listUserRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + List userInfo = JSON.parseArray(new String(response.getBody()), UserInfo.class); + assertThat(userInfo.get(0).getUsername()).isEqualTo("abc"); + assertThat(userInfo.get(0).getPassword()).isEqualTo("123"); + assertThat(userInfo.get(0).getUserType()).isEqualTo("Normal"); + } + + @Test + public void testCreateAcl() throws RemotingCommandException { + when(authorizationMetadataManager.createAcl(any(Acl.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + + CreateAclRequestHeader createAclRequestHeader = new CreateAclRequestHeader(); + createAclRequestHeader.setSubject("User:abc"); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.AUTH_CREATE_ACL, createAclRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + AclInfo aclInfo = AclInfo.of("User:abc", Arrays.asList("Topic:*"), Arrays.asList("PUB"), Arrays.asList("192.168.0.1"), "Grant"); + request.setBody(JSON.toJSONBytes(aclInfo)); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testUpdateAcl() throws RemotingCommandException { + when(authorizationMetadataManager.updateAcl(any(Acl.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + + UpdateAclRequestHeader updateAclRequestHeader = new UpdateAclRequestHeader(); + updateAclRequestHeader.setSubject("User:abc"); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.AUTH_UPDATE_ACL, updateAclRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + AclInfo aclInfo = AclInfo.of("User:abc", Arrays.asList("Topic:*"), Arrays.asList("PUB"), Arrays.asList("192.168.0.1"), "Grant"); + request.setBody(JSON.toJSONBytes(aclInfo)); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testDeleteAcl() throws RemotingCommandException { + when(authorizationMetadataManager.deleteAcl(any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + DeleteAclRequestHeader deleteAclRequestHeader = new DeleteAclRequestHeader(); + deleteAclRequestHeader.setSubject("User:abc"); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.AUTH_DELETE_ACL, deleteAclRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetAcl() throws RemotingCommandException { + Acl aclInfo = Acl.of(User.of("abc"), Arrays.asList(Resource.of("Topic:*")), Arrays.asList(Action.PUB), Environment.of("192.168.0.1"), Decision.ALLOW); + when(authorizationMetadataManager.getAcl(any(Subject.class))).thenReturn(CompletableFuture.completedFuture(aclInfo)); + + GetAclRequestHeader getAclRequestHeader = new GetAclRequestHeader(); + getAclRequestHeader.setSubject("User:abc"); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.AUTH_GET_ACL, getAclRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + AclInfo aclInfoData = JSON.parseObject(new String(response.getBody()), AclInfo.class); + assertThat(aclInfoData.getSubject()).isEqualTo("User:abc"); + assertThat(aclInfoData.getPolicies().get(0).getEntries().get(0).getResource()).isEqualTo("Topic:*"); + assertThat(aclInfoData.getPolicies().get(0).getEntries().get(0).getActions()).containsAll(Arrays.asList(Action.PUB.getName())); + assertThat(aclInfoData.getPolicies().get(0).getEntries().get(0).getSourceIps()).containsAll(Arrays.asList("192.168.0.1")); + assertThat(aclInfoData.getPolicies().get(0).getEntries().get(0).getDecision()).isEqualTo("Allow"); + } + + @Test + public void testListAcl() throws RemotingCommandException { + Acl aclInfo = Acl.of(User.of("abc"), Arrays.asList(Resource.of("Topic:*")), Arrays.asList(Action.PUB), Environment.of("192.168.0.1"), Decision.ALLOW); + when(authorizationMetadataManager.listAcl(any(), any())).thenReturn(CompletableFuture.completedFuture(Arrays.asList(aclInfo))); + + ListAclsRequestHeader listAclRequestHeader = new ListAclsRequestHeader(); + listAclRequestHeader.setSubjectFilter("User:abc"); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.AUTH_LIST_ACL, listAclRequestHeader); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + List aclInfoData = JSON.parseArray(new String(response.getBody()), AclInfo.class); + assertThat(aclInfoData.get(0).getSubject()).isEqualTo("User:abc"); + assertThat(aclInfoData.get(0).getPolicies().get(0).getEntries().get(0).getResource()).isEqualTo("Topic:*"); + assertThat(aclInfoData.get(0).getPolicies().get(0).getEntries().get(0).getActions()).containsAll(Arrays.asList(Action.PUB.getName())); + assertThat(aclInfoData.get(0).getPolicies().get(0).getEntries().get(0).getSourceIps()).containsAll(Arrays.asList("192.168.0.1")); + assertThat(aclInfoData.get(0).getPolicies().get(0).getEntries().get(0).getDecision()).isEqualTo("Allow"); + } + + @Test + public void testGetTimeCheckPoint() throws RemotingCommandException { + when(this.brokerController.getTimerCheckpoint()).thenReturn(null); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_TIMER_CHECK_POINT, null); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SYSTEM_ERROR); + assertThat(response.getRemark()).isEqualTo("The checkpoint is null"); + + when(this.brokerController.getTimerCheckpoint()).thenReturn(new TimerCheckpoint()); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + @Test + public void testGetTimeMetrics() throws RemotingCommandException, IOException { + when(this.brokerController.getMessageStore().getTimerMessageStore()).thenReturn(null); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_TIMER_METRICS, null); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SYSTEM_ERROR); + + when(this.brokerController.getMessageStore().getTimerMessageStore()).thenReturn(timerMessageStore); + when(this.timerMessageStore.getTimerMetrics()).thenReturn(timerMetrics); + when(this.timerMetrics.encode()).thenReturn(new TimerMetrics.TimerMetricsSerializeWrapper().toJson(false)); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testUpdateColdDataFlowCtrGroupConfig() throws RemotingCommandException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_COLD_DATA_FLOW_CTR_CONFIG, null); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + request.setBody("consumerGroup1=1".getBytes()); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + request.setBody("".getBytes()); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testRemoveColdDataFlowCtrGroupConfig() throws RemotingCommandException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.REMOVE_COLD_DATA_FLOW_CTR_CONFIG, null); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + request.setBody("consumerGroup1".getBytes()); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetColdDataFlowCtrInfo() throws RemotingCommandException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_COLD_DATA_FLOW_CTR_INFO, null); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testSetCommitLogReadAheadMode() throws RemotingCommandException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.SET_COMMITLOG_READ_MODE, null); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SYSTEM_ERROR); + + HashMap extfields = new HashMap<>(); + extfields.put(FIleReadaheadMode.READ_AHEAD_MODE, String.valueOf(LibC.MADV_DONTNEED)); + request.setExtFields(extfields); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.INVALID_PARAMETER); + + extfields.clear(); + extfields.put(FIleReadaheadMode.READ_AHEAD_MODE, String.valueOf(LibC.MADV_NORMAL)); + request.setExtFields(extfields); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + this.brokerController.setMessageStore(defaultMessageStore); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SYSTEM_ERROR); + + when(this.defaultMessageStore.getMessageStoreConfig()).thenReturn(messageStoreConfig); + when(this.defaultMessageStore.getCommitLog()).thenReturn(commitLog); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetUnknownCmdResponse() throws RemotingCommandException { + RemotingCommand request = RemotingCommand.createRequestCommand(10000, null); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.REQUEST_CODE_NOT_SUPPORTED); + } + + @Test + public void testGetAllMessageRequestMode() throws RemotingCommandException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ALL_MESSAGE_REQUEST_MODE, null); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testResetOffset() throws RemotingCommandException { + ResetOffsetRequestHeader requestHeader = + createRequestHeader("topic","group",-1,false,-1,-1); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.INVOKE_BROKER_TO_RESET_OFFSET, requestHeader); + request.makeCustomHeaderToNet(); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.TOPIC_NOT_EXIST); + + this.brokerController.getTopicConfigManager().getTopicConfigTable().put("topic", new TopicConfig("topic")); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUBSCRIPTION_GROUP_NOT_EXIST); + + this.brokerController.getSubscriptionGroupManager().getSubscriptionGroupTable().put("group", new SubscriptionGroupConfig()); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + requestHeader.setQueueId(0); + request = RemotingCommand.createRequestCommand(RequestCode.INVOKE_BROKER_TO_RESET_OFFSET, requestHeader); + request.makeCustomHeaderToNet(); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + requestHeader.setOffset(2L); + request = RemotingCommand.createRequestCommand(RequestCode.INVOKE_BROKER_TO_RESET_OFFSET, requestHeader); + request.makeCustomHeaderToNet(); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SYSTEM_ERROR); + } + + @Test + public void testGetConsumerStatus() throws RemotingCommandException { + GetConsumerStatusRequestHeader requestHeader = new GetConsumerStatusRequestHeader(); + requestHeader.setGroup("group"); + requestHeader.setTopic("topic"); + requestHeader.setClientAddr(""); + RemotingCommand request = RemotingCommand + .createRequestCommand(RequestCode.INVOKE_BROKER_TO_GET_CONSUMER_STATUS, requestHeader); + RemotingCommand responseCommand = RemotingCommand.createResponseCommand(null); + responseCommand.setCode(ResponseCode.SUCCESS); + when(broker2Client.getConsumeStatus(anyString(),anyString(),anyString())).thenReturn(responseCommand); + request.makeCustomHeaderToNet(); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testQueryTopicConsumeByWho() throws RemotingCommandException { + QueryTopicConsumeByWhoRequestHeader requestHeader = new QueryTopicConsumeByWhoRequestHeader(); + requestHeader.setTopic("topic"); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_TOPIC_CONSUME_BY_WHO, requestHeader); + request.makeCustomHeaderToNet(); + HashSet groups = new HashSet<>(); + groups.add("group"); + when(brokerController.getConsumerManager()).thenReturn(consumerManager); + when(consumerManager.queryTopicConsumeByWho(anyString())).thenReturn(groups); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(RemotingSerializable.decode(response.getBody(), GroupList.class) + .getGroupList().contains("group")) + .isEqualTo(groups.contains("group")); + } + + @Test + public void testQueryTopicByConsumer() throws RemotingCommandException { + QueryTopicsByConsumerRequestHeader requestHeader = new QueryTopicsByConsumerRequestHeader(); + requestHeader.setGroup("group"); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_TOPICS_BY_CONSUMER, requestHeader); + request.makeCustomHeaderToNet(); + when(brokerController.getConsumerOffsetManager()).thenReturn(consumerOffsetManager); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testQuerySubscriptionByConsumer() throws RemotingCommandException { + QuerySubscriptionByConsumerRequestHeader requestHeader = new QuerySubscriptionByConsumerRequestHeader(); + requestHeader.setGroup("group"); + requestHeader.setTopic("topic"); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_SUBSCRIPTION_BY_CONSUMER, requestHeader); + request.makeCustomHeaderToNet(); + when(brokerController.getConsumerManager()).thenReturn(consumerManager); + when(consumerManager.findSubscriptionData(anyString(),anyString())).thenReturn(null); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetSystemTopicListFromBroker() throws RemotingCommandException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_SYSTEM_TOPIC_LIST_FROM_BROKER, null); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testCleanExpiredConsumeQueue() throws RemotingCommandException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CLEAN_EXPIRED_CONSUMEQUEUE, null); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testDeleteExpiredCommitLog() throws RemotingCommandException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.DELETE_EXPIRED_COMMITLOG, null); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testCleanUnusedTopic() throws RemotingCommandException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CLEAN_UNUSED_TOPIC, null); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetConsumerRunningInfo() throws RemotingCommandException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException { + when(brokerController.getConsumerManager()).thenReturn(consumerManager); + when(consumerManager.findChannel(anyString(),anyString())).thenReturn(null); + GetConsumerRunningInfoRequestHeader requestHeader = new GetConsumerRunningInfoRequestHeader(); + requestHeader.setClientId("client"); + requestHeader.setConsumerGroup("group"); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_CONSUMER_RUNNING_INFO, requestHeader); + request.makeCustomHeaderToNet(); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SYSTEM_ERROR); + + when(consumerManager.findChannel(anyString(),anyString())).thenReturn(clientChannelInfo); + when(clientChannelInfo.getVersion()).thenReturn(MQVersion.Version.V3_0_0_SNAPSHOT.ordinal()); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SYSTEM_ERROR); + + when(clientChannelInfo.getVersion()).thenReturn(MQVersion.Version.V5_2_3.ordinal()); + when(brokerController.getBroker2Client()).thenReturn(broker2Client); + when(clientChannelInfo.getChannel()).thenReturn(channel); + RemotingCommand responseCommand = RemotingCommand.createResponseCommand(null); + responseCommand.setCode(ResponseCode.SUCCESS); + when(broker2Client.callClient(any(Channel.class),any(RemotingCommand.class))).thenReturn(responseCommand); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + when(broker2Client.callClient(any(Channel.class),any(RemotingCommand.class))).thenThrow(new RemotingTimeoutException("timeout")); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.CONSUME_MSG_TIMEOUT); + } + + @Test + public void testQueryCorrectionOffset() throws RemotingCommandException { + Map correctionOffsetMap = new HashMap<>(); + correctionOffsetMap.put(0, 100L); + correctionOffsetMap.put(1, 200L); + Map compareOffsetMap = new HashMap<>(); + compareOffsetMap.put(0, 80L); + compareOffsetMap.put(1, 300L); + when(brokerController.getConsumerOffsetManager()).thenReturn(consumerOffsetManager); + when(consumerOffsetManager.queryMinOffsetInAllGroup(anyString(),anyString())).thenReturn(correctionOffsetMap); + when(consumerOffsetManager.queryOffset(anyString(),anyString())).thenReturn(compareOffsetMap); + QueryCorrectionOffsetHeader queryCorrectionOffsetHeader = new QueryCorrectionOffsetHeader(); + queryCorrectionOffsetHeader.setTopic("topic"); + queryCorrectionOffsetHeader.setCompareGroup("group"); + queryCorrectionOffsetHeader.setFilterGroups(""); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_CORRECTION_OFFSET, queryCorrectionOffsetHeader); + request.makeCustomHeaderToNet(); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + QueryCorrectionOffsetBody body = RemotingSerializable.decode(response.getBody(), QueryCorrectionOffsetBody.class); + Map correctionOffsets = body.getCorrectionOffsets(); + assertThat(correctionOffsets.get(0)).isEqualTo(Long.MAX_VALUE); + assertThat(correctionOffsets.get(1)).isEqualTo(200L); + } + + @Test + public void testNotifyMinBrokerIdChange() throws RemotingCommandException { + NotifyMinBrokerIdChangeRequestHeader requestHeader = new NotifyMinBrokerIdChangeRequestHeader(); + requestHeader.setMinBrokerId(1L); + requestHeader.setMinBrokerAddr("127.0.0.1:10912"); + requestHeader.setOfflineBrokerAddr("127.0.0.1:10911"); + requestHeader.setHaBrokerAddr(""); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.NOTIFY_MIN_BROKER_ID_CHANGE, requestHeader); + request.makeCustomHeaderToNet(); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testUpdateBrokerHaInfo() throws RemotingCommandException { + ExchangeHAInfoResponseHeader requestHeader = new ExchangeHAInfoResponseHeader(); + requestHeader.setMasterAddress("127.0.0.1:10911"); + requestHeader.setMasterFlushOffset(0L); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.EXCHANGE_BROKER_HA_INFO, requestHeader); + request.makeCustomHeaderToNet(); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + when(brokerController.getMessageStore()).thenReturn(messageStore); + requestHeader.setMasterHaAddress("127.0.0.1:10912"); + request = RemotingCommand.createRequestCommand(RequestCode.EXCHANGE_BROKER_HA_INFO, requestHeader); + request.makeCustomHeaderToNet(); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + when(messageStore.getMasterFlushedOffset()).thenReturn(0L); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetBrokerHaStatus() throws RemotingCommandException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_BROKER_HA_STATUS,null); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SYSTEM_ERROR); + + when(brokerController.getMessageStore()).thenReturn(messageStore); + when(messageStore.getHARuntimeInfo()).thenReturn(new HARuntimeInfo()); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testResetMasterFlushOffset() throws RemotingCommandException { + ResetMasterFlushOffsetHeader requestHeader = new ResetMasterFlushOffsetHeader(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.RESET_MASTER_FLUSH_OFFSET,requestHeader); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + requestHeader.setMasterFlushOffset(0L); + request.makeCustomHeaderToNet(); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + private ResetOffsetRequestHeader createRequestHeader(String topic,String group,long timestamp,boolean force,long offset,int queueId) { + ResetOffsetRequestHeader requestHeader = new ResetOffsetRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setGroup(group); + requestHeader.setTimestamp(timestamp); + requestHeader.setForce(force); + requestHeader.setOffset(offset); + requestHeader.setQueueId(queueId); + return requestHeader; + } + + private RemotingCommand buildCreateTopicRequest(String topic) { + return buildCreateTopicRequest(topic, null); + } + + private RemotingCommand buildCreateTopicRequest(String topic, Map attributes) { + 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); + if (attributes != null) { + requestHeader.setAttributes(AttributeParser.parseToString(attributes)); + } + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_AND_CREATE_TOPIC, requestHeader); + request.makeCustomHeaderToNet(); + return request; + } + + private RemotingCommand buildCreateTopicListRequest(List topicList) { + return buildCreateTopicListRequest(topicList, null); + } + + private RemotingCommand buildCreateTopicListRequest(List topicList, Map attributes) { + List topicConfigList = new ArrayList<>(); + for (String topic:topicList) { + TopicConfig topicConfig = new TopicConfig(topic); + topicConfig.setReadQueueNums(8); + topicConfig.setWriteQueueNums(8); + topicConfig.setTopicFilterType(TopicFilterType.SINGLE_TAG); + topicConfig.setPerm(PermName.PERM_READ | PermName.PERM_WRITE); + topicConfig.setTopicSysFlag(0); + topicConfig.setOrder(false); + if (attributes != null) { + topicConfig.setAttributes(new HashMap<>(attributes)); + } + topicConfigList.add(topicConfig); + } + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_AND_CREATE_TOPIC_LIST, null); + CreateTopicListRequestBody createTopicListRequestBody = new CreateTopicListRequestBody(topicConfigList); + request.setBody(createTopicListRequestBody.encode()); + 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.setTopic("topic"); + 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 0000000..e15d51b --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessorTest.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.broker.processor; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import java.lang.reflect.Field; +import java.util.concurrent.CompletableFuture; +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.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.apache.rocketmq.store.exception.ConsumeQueueException; +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.anyString; +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(), null); + @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, ConsumeQueueException { + when(messageStore.getMaxOffsetInQueue(anyString(), anyInt())).thenReturn(2L); + when(escapeBridge.asyncPutMessageToSpecificQueue(any(MessageExtBrokerInner.class))).thenReturn(CompletableFuture.completedFuture(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()); + } + + @Test + public void testProcessRequest_NoMessage() throws RemotingCommandException, ConsumeQueueException { + when(messageStore.getMaxOffsetInQueue(anyString(), anyInt())).thenReturn(2L); + int queueId = 0; + long queueOffset = 2; + 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.NO_MESSAGE); + 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 new file mode 100644 index 0000000..874adb4 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/ClientManageProcessorTest.java @@ -0,0 +1,229 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.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; +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 ClientManageProcessorTest { + private ClientManageProcessor clientManageProcessor; + @Spy + private BrokerController brokerController = new BrokerController(new BrokerConfig(), new NettyServerConfig(), new NettyClientConfig(), new MessageStoreConfig()); + @Mock + private ChannelHandlerContext handlerContext; + @Mock + private Channel channel; + + private ClientChannelInfo clientChannelInfo; + private String clientId = UUID.randomUUID().toString(); + private String group = "FooBarGroup"; + private String topic = "FooBar"; + + @Before + public void init() { + when(handlerContext.channel()).thenReturn(channel); + clientManageProcessor = new ClientManageProcessor(brokerController); + clientChannelInfo = new ClientChannelInfo(channel, clientId, LanguageCode.JAVA, 100); + brokerController.getProducerManager().registerProducer(group, clientChannelInfo); + + ConsumerData consumerData = PullMessageProcessorTest.createConsumerData(group, topic); + brokerController.getConsumerManager().registerConsumer( + consumerData.getGroupName(), + clientChannelInfo, + consumerData.getConsumeType(), + consumerData.getMessageModel(), + consumerData.getConsumeFromWhere(), + consumerData.getSubscriptionDataSet(), + false); + } + + @Test + public void processRequest_UnRegisterProducer() throws Exception { + brokerController.getProducerManager().registerProducer(group, clientChannelInfo); + Map channelMap = brokerController.getProducerManager().getGroupChannelTable().get(group); + assertThat(channelMap).isNotNull(); + assertThat(channelMap.get(channel)).isEqualTo(clientChannelInfo); + + RemotingCommand request = createUnRegisterProducerCommand(); + RemotingCommand response = clientManageProcessor.processRequest(handlerContext, request); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + channelMap = brokerController.getProducerManager().getGroupChannelTable().get(group); + assertThat(channelMap).isNull(); + } + + @Test + public void processRequest_UnRegisterConsumer() throws RemotingCommandException { + ConsumerGroupInfo consumerGroupInfo = brokerController.getConsumerManager().getConsumerGroupInfo(group); + assertThat(consumerGroupInfo).isNotNull(); + + RemotingCommand request = createUnRegisterConsumerCommand(); + RemotingCommand response = clientManageProcessor.processRequest(handlerContext, request); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + consumerGroupInfo = brokerController.getConsumerManager().getConsumerGroupInfo(group); + 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); + requestHeader.setProducerGroup(group); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UNREGISTER_CLIENT, requestHeader); + request.setLanguage(LanguageCode.JAVA); + request.setVersion(100); + request.makeCustomHeaderToNet(); + return request; + } + + private RemotingCommand createUnRegisterConsumerCommand() { + UnregisterClientRequestHeader requestHeader = new UnregisterClientRequestHeader(); + requestHeader.setClientID(clientId); + requestHeader.setConsumerGroup(group); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UNREGISTER_CLIENT, requestHeader); + request.setLanguage(LanguageCode.JAVA); + request.setVersion(100); + request.makeCustomHeaderToNet(); + return request; + } + + 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 0000000..6b3c257 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/ConsumerManageProcessorTest.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.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.ConsumerGroupInfo; +import org.apache.rocketmq.broker.offset.ConsumerOffsetManager; +import org.apache.rocketmq.broker.out.BrokerOuterAPI; +import org.apache.rocketmq.broker.subscription.SubscriptionGroupManager; +import org.apache.rocketmq.broker.topic.TopicConfigManager; +import org.apache.rocketmq.broker.topic.TopicQueueMappingManager; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.TopicConfig; +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.GetConsumerListByGroupRequestHeader; +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.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.remoting.rpc.RpcClient; +import org.apache.rocketmq.remoting.rpc.RpcException; +import org.apache.rocketmq.remoting.rpc.RpcResponse; +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 java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +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.mock; +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; + @Mock + private Channel channel; + @Mock + private ConsumerOffsetManager consumerOffsetManager; + @Mock + private BrokerOuterAPI brokerOuterAPI; + @Mock + private RpcClient rpcClient; + @Mock + private Future responseFuture; + @Mock + private TopicQueueMappingContext mappingContext; + + private String topic = "FooBar"; + private String group = "FooBarGroup"; + + @Before + public void init() throws RpcException { + brokerController.setMessageStore(messageStore); + TopicConfigManager topicConfigManager = new TopicConfigManager(brokerController); + topicConfigManager.getTopicConfigTable().put(topic, new TopicConfig(topic)); + when(brokerController.getTopicConfigManager()).thenReturn(topicConfigManager); + SubscriptionGroupManager subscriptionGroupManager = new SubscriptionGroupManager(brokerController); + subscriptionGroupManager.getSubscriptionGroupTable().put(group, new SubscriptionGroupConfig()); + when(brokerController.getSubscriptionGroupManager()).thenReturn(subscriptionGroupManager); + consumerManageProcessor = new ConsumerManageProcessor(brokerController); + when(brokerController.getBrokerOuterAPI()).thenReturn(brokerOuterAPI); + when(brokerOuterAPI.getRpcClient()).thenReturn(rpcClient); + when(rpcClient.invoke(any(),anyLong())).thenReturn(responseFuture); + TopicQueueMappingDetail topicQueueMappingDetail = new TopicQueueMappingDetail(); + topicQueueMappingDetail.setBname("BrokerA"); + when(mappingContext.getMappingDetail()).thenReturn(topicQueueMappingDetail); + } + + @Test + public void testUpdateConsumerOffset_InvalidTopic() throws Exception { + RemotingCommand request = buildUpdateConsumerOffsetRequest(group, "InvalidTopic", 0, 0); + RemotingCommand response = consumerManageProcessor.processRequest(handlerContext, request); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.TOPIC_NOT_EXIST); + } + + @Test + public void testUpdateConsumerOffset_GroupNotExist() throws Exception { + RemotingCommand request = buildUpdateConsumerOffsetRequest("NotExistGroup", topic, 0, 0); + RemotingCommand response = consumerManageProcessor.processRequest(handlerContext, request); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUBSCRIPTION_GROUP_NOT_EXIST); + } + + @Test + public void testUpdateConsumerOffset() throws RemotingCommandException { + when(brokerController.getConsumerOffsetManager()).thenReturn(consumerOffsetManager); + when(consumerOffsetManager.hasOffsetReset(anyString(),anyString(),anyInt())).thenReturn(true); + RemotingCommand request = buildUpdateConsumerOffsetRequest(group, topic, 0, 0); + RemotingCommand response = consumerManageProcessor.processRequest(handlerContext, request); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + when(consumerOffsetManager.hasOffsetReset(anyString(),anyString(),anyInt())).thenReturn(false); + response = consumerManageProcessor.processRequest(handlerContext, request); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetConsumerListByGroup() throws RemotingCommandException { + GetConsumerListByGroupRequestHeader requestHeader = new GetConsumerListByGroupRequestHeader(); + requestHeader.setConsumerGroup(group); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_CONSUMER_LIST_BY_GROUP, requestHeader); + request.makeCustomHeaderToNet(); + RemotingCommand response = consumerManageProcessor.processRequest(handlerContext, request); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SYSTEM_ERROR); + + brokerController.getConsumerManager().getConsumerTable().put(group,new ConsumerGroupInfo(group)); + response = consumerManageProcessor.processRequest(handlerContext, request); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SYSTEM_ERROR); + + ConsumerGroupInfo consumerGroupInfo = + this.brokerController.getConsumerManager().getConsumerGroupInfo( + requestHeader.getConsumerGroup()); + consumerGroupInfo.getChannelInfoTable().put(channel,new ClientChannelInfo(channel)); + response = consumerManageProcessor.processRequest(handlerContext, request); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testQueryConsumerOffset() throws RemotingCommandException, ExecutionException, InterruptedException { + RemotingCommand request = buildQueryConsumerOffsetRequest(group, topic, 0, true); + RemotingCommand response = consumerManageProcessor.processRequest(handlerContext, request); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.QUERY_NOT_FOUND); + + when(brokerController.getConsumerOffsetManager()).thenReturn(consumerOffsetManager); + when(consumerOffsetManager.queryOffset(anyString(),anyString(),anyInt())).thenReturn(0L); + response = consumerManageProcessor.processRequest(handlerContext, request); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + when(consumerOffsetManager.queryOffset(anyString(),anyString(),anyInt())).thenReturn(-1L); + when(messageStore.getMinOffsetInQueue(anyString(),anyInt())).thenReturn(-1L); + when(messageStore.checkInMemByConsumeOffset(anyString(),anyInt(),anyLong(),anyInt())).thenReturn(true); + response = consumerManageProcessor.processRequest(handlerContext, request); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + TopicQueueMappingManager topicQueueMappingManager = mock(TopicQueueMappingManager.class); + when(brokerController.getTopicQueueMappingManager()).thenReturn(topicQueueMappingManager); + when(topicQueueMappingManager.buildTopicQueueMappingContext(any(QueryConsumerOffsetRequestHeader.class))).thenReturn(mappingContext); + response = consumerManageProcessor.processRequest(handlerContext, request); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.NOT_LEADER_FOR_QUEUE); + + List items = new ArrayList<>(); + LogicQueueMappingItem item1 = createLogicQueueMappingItem("BrokerC", 0, 0L, 0L); + items.add(item1); + when(mappingContext.getMappingItemList()).thenReturn(items); + when(mappingContext.getLeaderItem()).thenReturn(item1); + when(mappingContext.getCurrentItem()).thenReturn(item1); + when(mappingContext.isLeader()).thenReturn(true); + response = consumerManageProcessor.processRequest(handlerContext, request); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + LogicQueueMappingItem item2 = createLogicQueueMappingItem("BrokerA", 0, 0L, 0L); + items.add(item2); + QueryConsumerOffsetResponseHeader queryConsumerOffsetResponseHeader = new QueryConsumerOffsetResponseHeader(); + queryConsumerOffsetResponseHeader.setOffset(0L); + RpcResponse rpcResponse = new RpcResponse(ResponseCode.SUCCESS,queryConsumerOffsetResponseHeader,null); + when(responseFuture.get()).thenReturn(rpcResponse); + response = consumerManageProcessor.processRequest(handlerContext, request); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + queryConsumerOffsetResponseHeader.setOffset(-1L); + rpcResponse = new RpcResponse(ResponseCode.SUCCESS,queryConsumerOffsetResponseHeader,null); + when(responseFuture.get()).thenReturn(rpcResponse); + response = consumerManageProcessor.processRequest(handlerContext, request); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.QUERY_NOT_FOUND); + } + + @Test + public void testRewriteRequestForStaticTopic() throws RpcException, ExecutionException, InterruptedException { + UpdateConsumerOffsetRequestHeader requestHeader = new UpdateConsumerOffsetRequestHeader(); + requestHeader.setConsumerGroup(group); + requestHeader.setTopic(topic); + requestHeader.setQueueId(0); + requestHeader.setCommitOffset(0L); + + RemotingCommand response = consumerManageProcessor.rewriteRequestForStaticTopic(requestHeader, mappingContext); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.NOT_LEADER_FOR_QUEUE); + + List items = new ArrayList<>(); + LogicQueueMappingItem item = createLogicQueueMappingItem("BrokerC", 0, 0L, 0L); + items.add(item); + when(mappingContext.getMappingItemList()).thenReturn(items); + when(mappingContext.isLeader()).thenReturn(true); + RpcResponse rpcResponse = new RpcResponse(ResponseCode.SUCCESS,new UpdateConsumerOffsetResponseHeader(),null); + when(responseFuture.get()).thenReturn(rpcResponse); + response = consumerManageProcessor.rewriteRequestForStaticTopic(requestHeader, mappingContext); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + public RemotingCommand buildQueryConsumerOffsetRequest(String group, String topic, int queueId,boolean setZeroIfNotFound) { + QueryConsumerOffsetRequestHeader requestHeader = new QueryConsumerOffsetRequestHeader(); + requestHeader.setConsumerGroup(group); + requestHeader.setTopic(topic); + requestHeader.setQueueId(queueId); + requestHeader.setSetZeroIfNotFound(setZeroIfNotFound); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_CONSUMER_OFFSET, requestHeader); + request.makeCustomHeaderToNet(); + return request; + } + + public LogicQueueMappingItem createLogicQueueMappingItem(String brokerName, int queueId, long startOffset, long logicOffset) { + LogicQueueMappingItem item = new LogicQueueMappingItem(); + item.setBname(brokerName); + item.setQueueId(queueId); + item.setStartOffset(startOffset); + item.setLogicOffset(logicOffset); + return item; + } + + private RemotingCommand buildUpdateConsumerOffsetRequest(String group, String topic, int queueId, long offset) { + UpdateConsumerOffsetRequestHeader requestHeader = new UpdateConsumerOffsetRequestHeader(); + requestHeader.setConsumerGroup(group); + requestHeader.setTopic(topic); + requestHeader.setQueueId(queueId); + requestHeader.setCommitOffset(offset); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_CONSUMER_OFFSET, requestHeader); + 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 0000000..e4360f1 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/EndTransactionProcessorTest.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.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.stats.Stats; +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 static final String TOPIC = "trans_topic_test"; + + private EndTransactionProcessor endTransactionProcessor; + + @Mock + private ChannelHandlerContext handlerContext; + + @Spy + private BrokerController + brokerController = new BrokerController(new BrokerConfig(), new NettyServerConfig(), new NettyClientConfig(), + new MessageStoreConfig(), null); + + @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, createAppendMessageResult(AppendMessageStatus.PUT_OK))); + RemotingCommand request = createEndTransactionMsgCommand(MessageSysFlag.TRANSACTION_COMMIT_TYPE, false); + RemotingCommand response = endTransactionProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(brokerController.getBrokerStatsManager().getStatsItem(Stats.BROKER_PUT_NUMS, brokerController.getBrokerConfig().getBrokerClusterName()).getValue().sum()).isEqualTo(1); + assertThat(brokerController.getBrokerStatsManager().getStatsItem(Stats.TOPIC_PUT_NUMS, TOPIC).getValue().sum()).isEqualTo(1L); + assertThat(brokerController.getBrokerStatsManager().getStatsItem(Stats.TOPIC_PUT_SIZE, TOPIC).getValue().sum()).isEqualTo(1L); + } + + @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, createAppendMessageResult(AppendMessageStatus.PUT_OK))); + RemotingCommand request = createEndTransactionMsgCommand(MessageSysFlag.TRANSACTION_COMMIT_TYPE, true); + RemotingCommand response = endTransactionProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(brokerController.getBrokerStatsManager().getStatsItem(Stats.BROKER_PUT_NUMS, brokerController.getBrokerConfig().getBrokerClusterName()).getValue().sum()).isEqualTo(1); + assertThat(brokerController.getBrokerStatsManager().getStatsItem(Stats.TOPIC_PUT_NUMS, TOPIC).getValue().sum()).isEqualTo(1L); + assertThat(brokerController.getBrokerStatsManager().getStatsItem(Stats.TOPIC_PUT_SIZE, TOPIC).getValue().sum()).isEqualTo(1L); + } + + @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_TOPIC, TOPIC); + 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.setTopic("topic"); + 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; + } + + private AppendMessageResult createAppendMessageResult(AppendMessageStatus status) { + AppendMessageResult result = new AppendMessageResult(status); + result.setMsgId("12345678"); + result.setMsgNum(1); + result.setWroteBytes(1); + return result; + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/processor/PeekMessageProcessorTest.java b/broker/src/test/java/org/apache/rocketmq/broker/processor/PeekMessageProcessorTest.java new file mode 100644 index 0000000..9baf2a6 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/PeekMessageProcessorTest.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.broker.processor; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +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.TopicConfig; +import org.apache.rocketmq.common.constant.PermName; +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.PeekMessageRequestHeader; +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.MessageStore; +import org.apache.rocketmq.store.SelectMappedBufferResult; +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.net.InetSocketAddress; +import java.nio.ByteBuffer; + +import static org.assertj.core.api.AssertionsForClassTypes.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 PeekMessageProcessorTest { + + private PeekMessageProcessor peekMessageProcessor; + + @Spy + private BrokerController brokerController = new BrokerController(new BrokerConfig(), new NettyServerConfig(), new NettyClientConfig(), new MessageStoreConfig()); + + @Mock + private ChannelHandlerContext handlerContext; + + @Mock + private MessageStore messageStore; + + @Mock + private SubscriptionGroupManager subscriptionGroupManager; + + @Mock + private ConsumerOffsetManager consumerOffsetManager; + + @Mock + private SubscriptionGroupConfig subscriptionGroupConfig; + + @Mock + private Channel channel; + + private TopicConfigManager topicConfigManager; + + @Before + public void init() { + peekMessageProcessor = new PeekMessageProcessor(brokerController); + when(brokerController.getMessageStore()).thenReturn(messageStore); + topicConfigManager = new TopicConfigManager(brokerController); + when(brokerController.getTopicConfigManager()).thenReturn(topicConfigManager); + when(brokerController.getSubscriptionGroupManager()).thenReturn(subscriptionGroupManager); + when(subscriptionGroupManager.findSubscriptionGroupConfig(anyString())).thenReturn(subscriptionGroupConfig); + when(subscriptionGroupConfig.isConsumeEnable()).thenReturn(true); + topicConfigManager.getTopicConfigTable().put("topic", new TopicConfig("topic")); + when(brokerController.getConsumerOffsetManager()).thenReturn(consumerOffsetManager); + when(consumerOffsetManager.queryOffset(anyString(), anyString(), anyInt())).thenReturn(-1L); + when(messageStore.getMinOffsetInQueue(anyString(),anyInt())).thenReturn(0L); + when(handlerContext.channel()).thenReturn(channel); + when(channel.remoteAddress()).thenReturn(new InetSocketAddress("127.0.0.1", 12345)); + } + + @Test + public void testProcessRequest() throws RemotingCommandException { + RemotingCommand request = createPeekMessageRequest("group","topic",0); + GetMessageResult getMessageResult = new GetMessageResult(); + getMessageResult.setStatus(GetMessageStatus.FOUND); + ByteBuffer bb = ByteBuffer.allocate(64); + bb.putLong(MessageDecoder.MESSAGE_STORE_TIMESTAMP_POSITION, System.currentTimeMillis()); + SelectMappedBufferResult mappedBufferResult1 = new SelectMappedBufferResult(0, bb, 64, null); + for (int i = 0; i < 10;i++) { + getMessageResult.addMessage(mappedBufferResult1); + } + when(messageStore.getMessage(anyString(),anyString(),anyInt(),anyLong(),anyInt(),any())).thenReturn(getMessageResult); + RemotingCommand response = peekMessageProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testProcessRequest_NoPermission() throws RemotingCommandException { + this.brokerController.getBrokerConfig().setBrokerPermission(PermName.PERM_WRITE); + RemotingCommand request = createPeekMessageRequest("group","topic",0); + RemotingCommand response = peekMessageProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.NO_PERMISSION); + this.brokerController.getBrokerConfig().setBrokerPermission(PermName.PERM_WRITE | PermName.PERM_READ); + + topicConfigManager.getTopicConfigTable().get("topic").setPerm(PermName.PERM_WRITE); + response = peekMessageProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.NO_PERMISSION); + topicConfigManager.getTopicConfigTable().get("topic").setPerm(PermName.PERM_WRITE | PermName.PERM_READ); + + when(subscriptionGroupConfig.isConsumeEnable()).thenReturn(false); + response = peekMessageProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.NO_PERMISSION); + } + + @Test + public void testProcessRequest_TopicNotExist() throws RemotingCommandException { + RemotingCommand request = createPeekMessageRequest("group1","topic1",0); + RemotingCommand response = peekMessageProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.TOPIC_NOT_EXIST); + } + + @Test + public void testProcessRequest_SubscriptionGroupNotExist() throws RemotingCommandException { + when(subscriptionGroupManager.findSubscriptionGroupConfig(anyString())).thenReturn(null); + RemotingCommand request = createPeekMessageRequest("group","topic",0); + RemotingCommand response = peekMessageProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUBSCRIPTION_GROUP_NOT_EXIST); + } + + @Test + public void testProcessRequest_QueueIdError() throws RemotingCommandException { + RemotingCommand request = createPeekMessageRequest("group","topic",17); + RemotingCommand response = peekMessageProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.INVALID_PARAMETER); + } + + private RemotingCommand createPeekMessageRequest(String group,String topic,int queueId) { + PeekMessageRequestHeader peekMessageRequestHeader = new PeekMessageRequestHeader(); + peekMessageRequestHeader.setConsumerGroup(group); + peekMessageRequestHeader.setTopic(topic); + peekMessageRequestHeader.setMaxMsgNums(10); + peekMessageRequestHeader.setQueueId(queueId); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PEEK_MESSAGE, peekMessageRequestHeader); + request.makeCustomHeaderToNet(); + return request; + } +} 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 0000000..acc7a3d --- /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 0000000..dea59fc --- /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 0000000..fdb0690 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/PopMessageProcessorTest.java @@ -0,0 +1,243 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.KeyBuilder; +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.exception.ConsumeQueueException; +import org.apache.rocketmq.store.logfile.DefaultMappedFile; +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.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.eq; +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"); + } + + @Test + public void testGetInitOffset_retryTopic() throws RemotingCommandException { + when(messageStore.getMessageStoreConfig()).thenReturn(new MessageStoreConfig()); + String newGroup = group + "-" + System.currentTimeMillis(); + String retryTopic = KeyBuilder.buildPopRetryTopic(topic, newGroup); + long minOffset = 100L; + when(messageStore.getMinOffsetInQueue(retryTopic, 0)).thenReturn(minOffset); + brokerController.getTopicConfigManager().getTopicConfigTable().put(retryTopic, new TopicConfig(retryTopic, 1, 1)); + GetMessageResult getMessageResult = createGetMessageResult(0); + when(messageStore.getMessageAsync(eq(newGroup), anyString(), anyInt(), anyLong(), anyInt(), any())) + .thenReturn(CompletableFuture.completedFuture(getMessageResult)); + + long offset = brokerController.getConsumerOffsetManager().queryOffset(newGroup, retryTopic, 0); + Assert.assertEquals(-1, offset); + + RemotingCommand request = createPopMsgCommand(newGroup, topic, 0, ConsumeInitMode.MAX); + popMessageProcessor.processRequest(handlerContext, request); + offset = brokerController.getConsumerOffsetManager().queryOffset(newGroup, retryTopic, 0); + Assert.assertEquals(minOffset, offset); + + when(messageStore.getMinOffsetInQueue(retryTopic, 0)).thenReturn(minOffset * 2); + popMessageProcessor.processRequest(handlerContext, request); + offset = brokerController.getConsumerOffsetManager().queryOffset(newGroup, retryTopic, 0); + Assert.assertEquals(minOffset, offset); // will not entry getInitOffset() again + messageStore.getMinOffsetInQueue(retryTopic, 0); // prevent UnnecessaryStubbingException + } + + @Test + public void testGetInitOffset_normalTopic() throws RemotingCommandException, ConsumeQueueException { + long maxOffset = 999L; + when(messageStore.getMessageStoreConfig()).thenReturn(new MessageStoreConfig()); + when(messageStore.getMaxOffsetInQueue(topic, 0)).thenReturn(maxOffset); + String newGroup = group + "-" + System.currentTimeMillis(); + GetMessageResult getMessageResult = createGetMessageResult(0); + when(messageStore.getMessageAsync(eq(newGroup), anyString(), anyInt(), anyLong(), anyInt(), any())) + .thenReturn(CompletableFuture.completedFuture(getMessageResult)); + + long offset = brokerController.getConsumerOffsetManager().queryOffset(newGroup, topic, 0); + Assert.assertEquals(-1, offset); + + RemotingCommand request = createPopMsgCommand(newGroup, topic, 0, ConsumeInitMode.MAX); + popMessageProcessor.processRequest(handlerContext, request); + offset = brokerController.getConsumerOffsetManager().queryOffset(newGroup, topic, 0); + Assert.assertEquals(maxOffset - 1, offset); // checkInMem return false + + when(messageStore.getMaxOffsetInQueue(topic, 0)).thenReturn(maxOffset * 2); + popMessageProcessor.processRequest(handlerContext, request); + offset = brokerController.getConsumerOffsetManager().queryOffset(newGroup, topic, 0); + Assert.assertEquals(maxOffset - 1, offset); // will not entry getInitOffset() again + messageStore.getMaxOffsetInQueue(topic, 0); // prevent UnnecessaryStubbingException + } + + private RemotingCommand createPopMsgCommand() { + return createPopMsgCommand(group, topic, -1, ConsumeInitMode.MAX); + } + + private RemotingCommand createPopMsgCommand(String group, String topic, int queueId, int initMode) { + PopMessageRequestHeader requestHeader = new PopMessageRequestHeader(); + requestHeader.setConsumerGroup(group); + requestHeader.setMaxMsgNums(30); + requestHeader.setQueueId(queueId); + requestHeader.setTopic(topic); + requestHeader.setInvisibleTime(10_000); + requestHeader.setInitMode(initMode); + 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 0000000..3010e83 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/PopReviveServiceTest.java @@ -0,0 +1,487 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.commons.lang3.tuple.Triple; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.failover.EscapeBridge; +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.KeyBuilder; +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.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.AppendMessageResult; +import org.apache.rocketmq.store.AppendMessageStatus; +import org.apache.rocketmq.store.pop.AckMsg; +import org.apache.rocketmq.store.pop.PopCheckPoint; +import org.apache.rocketmq.store.timer.TimerMessageStore; +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.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.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.times; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class PopReviveServiceTest { + + private static final String CLUSTER_NAME = "test"; + private static final String REVIVE_TOPIC = PopAckConstants.buildClusterReviveTopic(CLUSTER_NAME); + 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"); + private static final Long INVISIBLE_TIME = 1000L; + + @Mock + private MessageStore messageStore; + @Mock + private ConsumerOffsetManager consumerOffsetManager; + @Mock + private TopicConfigManager topicConfigManager; + @Mock + private TimerMessageStore timerMessageStore; + @Mock + private SubscriptionGroupManager subscriptionGroupManager; + @Mock + private BrokerController brokerController; + @Mock + private EscapeBridge escapeBridge; + private PopMessageProcessor popMessageProcessor; + + private BrokerConfig brokerConfig; + private PopReviveService popReviveService; + + @Before + public void before() { + brokerConfig = new BrokerConfig(); + brokerConfig.setBrokerClusterName(CLUSTER_NAME); + 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(brokerController.getEscapeBridge()).thenReturn(escapeBridge); + 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()); + + popMessageProcessor = new PopMessageProcessor(brokerController); // a real one, not mock + when(brokerController.getPopMessageProcessor()).thenReturn(popMessageProcessor); + + 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()); + } + + @Test + public void testReviveMsgFromCk_messageFound_writeRetryOK() throws Throwable { + PopCheckPoint ck = buildPopCheckPoint(0, 0, 0); + PopReviveService.ConsumeReviveObj reviveObj = new PopReviveService.ConsumeReviveObj(); + reviveObj.map.put("", ck); + reviveObj.endTime = System.currentTimeMillis(); + StringBuilder actualRetryTopic = new StringBuilder(); + + when(escapeBridge.getMessageAsync(anyString(), anyLong(), anyInt(), anyString(), anyBoolean())) + .thenReturn(CompletableFuture.completedFuture(Triple.of(new MessageExt(), "", false))); + when(escapeBridge.putMessageToSpecificQueue(any(MessageExtBrokerInner.class))).thenAnswer(invocation -> { + MessageExtBrokerInner msg = invocation.getArgument(0); + actualRetryTopic.append(msg.getTopic()); + return new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK)); + }); + + popReviveService.mergeAndRevive(reviveObj); + Assert.assertEquals(KeyBuilder.buildPopRetryTopic(TOPIC, GROUP, false), actualRetryTopic.toString()); + verify(escapeBridge, times(1)).putMessageToSpecificQueue(any(MessageExtBrokerInner.class)); // write retry + verify(messageStore, times(0)).putMessage(any(MessageExtBrokerInner.class)); // rewrite CK + } + + @Test + public void testReviveMsgFromCk_messageFound_writeRetryFailed_rewriteCK() throws Throwable { + PopCheckPoint ck = buildPopCheckPoint(0, 0, 0); + PopReviveService.ConsumeReviveObj reviveObj = new PopReviveService.ConsumeReviveObj(); + reviveObj.map.put("", ck); + reviveObj.endTime = System.currentTimeMillis(); + StringBuilder actualRetryTopic = new StringBuilder(); + StringBuilder actualReviveTopic = new StringBuilder(); + AtomicLong actualInvisibleTime = new AtomicLong(0L); + + when(escapeBridge.getMessageAsync(anyString(), anyLong(), anyInt(), anyString(), anyBoolean())) + .thenReturn(CompletableFuture.completedFuture(Triple.of(new MessageExt(), "", false))); + when(escapeBridge.putMessageToSpecificQueue(any(MessageExtBrokerInner.class))).thenAnswer(invocation -> { + MessageExtBrokerInner msg = invocation.getArgument(0); + actualRetryTopic.append(msg.getTopic()); + return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, new AppendMessageResult(AppendMessageStatus.MESSAGE_SIZE_EXCEEDED)); + }); + when(messageStore.putMessage(any(MessageExtBrokerInner.class))).thenAnswer(invocation -> { + MessageExtBrokerInner msg = invocation.getArgument(0); + actualReviveTopic.append(msg.getTopic()); + PopCheckPoint rewriteCK = JSON.parseObject(msg.getBody(), PopCheckPoint.class); + actualInvisibleTime.set(rewriteCK.getReviveTime()); + return new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK)); + }); + + popReviveService.mergeAndRevive(reviveObj); + Assert.assertEquals(KeyBuilder.buildPopRetryTopic(TOPIC, GROUP, false), actualRetryTopic.toString()); + Assert.assertEquals(REVIVE_TOPIC, actualReviveTopic.toString()); + Assert.assertEquals(INVISIBLE_TIME + 10 * 1000L, actualInvisibleTime.get()); // first interval is 10s + verify(escapeBridge, times(1)).putMessageToSpecificQueue(any(MessageExtBrokerInner.class)); // write retry + verify(messageStore, times(1)).putMessage(any(MessageExtBrokerInner.class)); // rewrite CK + } + + @Test + public void testReviveMsgFromCk_messageFound_writeRetryFailed_rewriteCK_end() throws Throwable { + brokerConfig.setSkipWhenCKRePutReachMaxTimes(true); + PopCheckPoint ck = buildPopCheckPoint(0, 0, 0); + ck.setRePutTimes("17"); + PopReviveService.ConsumeReviveObj reviveObj = new PopReviveService.ConsumeReviveObj(); + reviveObj.map.put("", ck); + reviveObj.endTime = System.currentTimeMillis(); + StringBuilder actualRetryTopic = new StringBuilder(); + + when(escapeBridge.getMessageAsync(anyString(), anyLong(), anyInt(), anyString(), anyBoolean())) + .thenReturn(CompletableFuture.completedFuture(Triple.of(new MessageExt(), "", false))); + when(escapeBridge.putMessageToSpecificQueue(any(MessageExtBrokerInner.class))).thenAnswer(invocation -> { + MessageExtBrokerInner msg = invocation.getArgument(0); + actualRetryTopic.append(msg.getTopic()); + return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, new AppendMessageResult(AppendMessageStatus.MESSAGE_SIZE_EXCEEDED)); + }); + + popReviveService.mergeAndRevive(reviveObj); + Assert.assertEquals(KeyBuilder.buildPopRetryTopic(TOPIC, GROUP, false), actualRetryTopic.toString()); + verify(escapeBridge, times(1)).putMessageToSpecificQueue(any(MessageExtBrokerInner.class)); // write retry + verify(messageStore, times(0)).putMessage(any(MessageExtBrokerInner.class)); // rewrite CK + } + + @Test + public void testReviveMsgFromCk_messageFound_writeRetryFailed_rewriteCK_noEnd() throws Throwable { + brokerConfig.setSkipWhenCKRePutReachMaxTimes(false); + PopCheckPoint ck = buildPopCheckPoint(0, 0, 0); + ck.setRePutTimes(Byte.MAX_VALUE + ""); + PopReviveService.ConsumeReviveObj reviveObj = new PopReviveService.ConsumeReviveObj(); + reviveObj.map.put("", ck); + reviveObj.endTime = System.currentTimeMillis(); + StringBuilder actualRetryTopic = new StringBuilder(); + + when(escapeBridge.getMessageAsync(anyString(), anyLong(), anyInt(), anyString(), anyBoolean())) + .thenReturn(CompletableFuture.completedFuture(Triple.of(new MessageExt(), "", false))); + when(escapeBridge.putMessageToSpecificQueue(any(MessageExtBrokerInner.class))).thenAnswer(invocation -> { + MessageExtBrokerInner msg = invocation.getArgument(0); + actualRetryTopic.append(msg.getTopic()); + return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, new AppendMessageResult(AppendMessageStatus.MESSAGE_SIZE_EXCEEDED)); + }); + + popReviveService.mergeAndRevive(reviveObj); + Assert.assertEquals(KeyBuilder.buildPopRetryTopic(TOPIC, GROUP, false), actualRetryTopic.toString()); + verify(escapeBridge, times(1)).putMessageToSpecificQueue(any(MessageExtBrokerInner.class)); // write retry + verify(messageStore, times(1)).putMessage(any(MessageExtBrokerInner.class)); // rewrite CK + } + + @Test + public void testReviveMsgFromCk_messageNotFound_noRetry() throws Throwable { + PopCheckPoint ck = buildPopCheckPoint(0, 0, 0); + PopReviveService.ConsumeReviveObj reviveObj = new PopReviveService.ConsumeReviveObj(); + reviveObj.map.put("", ck); + reviveObj.endTime = System.currentTimeMillis(); + + when(escapeBridge.getMessageAsync(anyString(), anyLong(), anyInt(), anyString(), anyBoolean())) + .thenReturn(CompletableFuture.completedFuture(Triple.of(null, "", false))); + + popReviveService.mergeAndRevive(reviveObj); + verify(escapeBridge, times(0)).putMessageToSpecificQueue(any(MessageExtBrokerInner.class)); // write retry + verify(messageStore, times(0)).putMessage(any(MessageExtBrokerInner.class)); // rewrite CK + } + + @Test + public void testReviveMsgFromCk_messageNotFound_needRetry() throws Throwable { + PopCheckPoint ck = buildPopCheckPoint(0, 0, 0); + PopReviveService.ConsumeReviveObj reviveObj = new PopReviveService.ConsumeReviveObj(); + reviveObj.map.put("", ck); + reviveObj.endTime = System.currentTimeMillis(); + StringBuilder actualReviveTopic = new StringBuilder(); + AtomicLong actualInvisibleTime = new AtomicLong(0L); + + when(escapeBridge.getMessageAsync(anyString(), anyLong(), anyInt(), anyString(), anyBoolean())) + .thenReturn(CompletableFuture.completedFuture(Triple.of(null, "", true))); + when(messageStore.putMessage(any(MessageExtBrokerInner.class))).thenAnswer(invocation -> { + MessageExtBrokerInner msg = invocation.getArgument(0); + actualReviveTopic.append(msg.getTopic()); + PopCheckPoint rewriteCK = JSON.parseObject(msg.getBody(), PopCheckPoint.class); + actualInvisibleTime.set(rewriteCK.getReviveTime()); + return new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK)); + }); + + popReviveService.mergeAndRevive(reviveObj); + Assert.assertEquals(REVIVE_TOPIC, actualReviveTopic.toString()); + Assert.assertEquals(INVISIBLE_TIME + 10 * 1000L, actualInvisibleTime.get()); // first interval is 10s + verify(escapeBridge, times(0)).putMessageToSpecificQueue(any(MessageExtBrokerInner.class)); // write retry + verify(messageStore, times(1)).putMessage(any(MessageExtBrokerInner.class)); // rewrite CK + } + + @Test + public void testReviveMsgFromCk_messageNotFound_needRetry_end() throws Throwable { + brokerConfig.setSkipWhenCKRePutReachMaxTimes(true); + PopCheckPoint ck = buildPopCheckPoint(0, 0, 0); + ck.setRePutTimes("17"); + PopReviveService.ConsumeReviveObj reviveObj = new PopReviveService.ConsumeReviveObj(); + reviveObj.map.put("", ck); + reviveObj.endTime = System.currentTimeMillis(); + + when(escapeBridge.getMessageAsync(anyString(), anyLong(), anyInt(), anyString(), anyBoolean())) + .thenReturn(CompletableFuture.completedFuture(Triple.of(null, "", true))); + + popReviveService.mergeAndRevive(reviveObj); + verify(escapeBridge, times(0)).putMessageToSpecificQueue(any(MessageExtBrokerInner.class)); // write retry + verify(messageStore, times(0)).putMessage(any(MessageExtBrokerInner.class)); // rewrite CK + } + + @Test + public void testReviveMsgFromCk_messageNotFound_needRetry_noEnd() throws Throwable { + brokerConfig.setSkipWhenCKRePutReachMaxTimes(false); + PopCheckPoint ck = buildPopCheckPoint(0, 0, 0); + ck.setRePutTimes(Byte.MAX_VALUE + ""); + PopReviveService.ConsumeReviveObj reviveObj = new PopReviveService.ConsumeReviveObj(); + reviveObj.map.put("", ck); + reviveObj.endTime = System.currentTimeMillis(); + + when(escapeBridge.getMessageAsync(anyString(), anyLong(), anyInt(), anyString(), anyBoolean())) + .thenReturn(CompletableFuture.completedFuture(Triple.of(null, "", true))); + + popReviveService.mergeAndRevive(reviveObj); + verify(escapeBridge, times(0)).putMessageToSpecificQueue(any(MessageExtBrokerInner.class)); // write retry + verify(messageStore, times(1)).putMessage(any(MessageExtBrokerInner.class)); // rewrite CK + } + + 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(INVISIBLE_TIME); + ck.setBrokerName("broker-a"); + 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); + ackMsg.setBrokerName("broker-a"); + + 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 new file mode 100644 index 0000000..83c3011 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/PullMessageProcessorTest.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.processor; + +import io.netty.channel.ChannelHandlerContext; +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.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; +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 PullMessageProcessorTest { + private PullMessageProcessor pullMessageProcessor; + @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 MessageStore messageStore; + private ClientChannelInfo clientChannelInfo; + private String group = "FooBarGroup"; + private String topic = "FooBar"; + + @Before + public void init() { + brokerController.setMessageStore(messageStore); + SubscriptionGroupManager subscriptionGroupManager = new SubscriptionGroupManager(brokerController); + pullMessageProcessor = new PullMessageProcessor(brokerController); + 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(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 { + brokerController.getTopicConfigManager().getTopicConfigTable().remove(topic); + final RemotingCommand request = createPullMsgCommand(RequestCode.PULL_MESSAGE); + RemotingCommand response = pullMessageProcessor.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_SubNotExist() throws RemotingCommandException { + brokerController.getConsumerManager().unregisterConsumer(group, clientChannelInfo, false); + final RemotingCommand request = createPullMsgCommand(RequestCode.PULL_MESSAGE); + RemotingCommand response = pullMessageProcessor.processRequest(handlerContext, request); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUBSCRIPTION_NOT_EXIST); + assertThat(response.getRemark()).contains("consumer's group info not exist"); + } + + @Test + public void testProcessRequest_SubNotLatest() throws RemotingCommandException { + final RemotingCommand request = createPullMsgCommand(RequestCode.PULL_MESSAGE); + request.addExtField("subVersion", String.valueOf(101)); + RemotingCommand response = pullMessageProcessor.processRequest(handlerContext, request); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUBSCRIPTION_NOT_LATEST); + assertThat(response.getRemark()).contains("subscription not latest"); + } + + @Test + public void testProcessRequest_Found() 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); + } + + @Test + public void testProcessRequest_FoundWithHook() throws RemotingCommandException { + GetMessageResult getMessageResult = createGetMessageResult(); + 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() { + @Override + public String hookName() { + return "TestHook"; + } + + @Override + public void consumeMessageBefore(ConsumeMessageContext context) { + messageContext[0] = context; + } + + @Override + public void consumeMessageAfter(ConsumeMessageContext context) { + } + }; + consumeMessageHookList.add(consumeMessageHook); + pullMessageProcessor.registerConsumeMessageHook(consumeMessageHookList); + 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(messageContext[0]).isNotNull(); + assertThat(messageContext[0].getConsumerGroup()).isEqualTo(group); + assertThat(messageContext[0].getTopic()).isEqualTo(topic); + assertThat(messageContext[0].getQueueId()).isEqualTo(1); + } + + @Test + public void testProcessRequest_MsgWasRemoving() throws RemotingCommandException { + GetMessageResult getMessageResult = createGetMessageResult(); + getMessageResult.setStatus(GetMessageStatus.MESSAGE_WAS_REMOVING); + 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.PULL_RETRY_IMMEDIATELY); + } + + @Test + public void testProcessRequest_NoMsgInQueue() throws RemotingCommandException { + GetMessageResult getMessageResult = createGetMessageResult(); + getMessageResult.setStatus(GetMessageStatus.NO_MESSAGE_IN_QUEUE); + 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.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); + requestHeader.setConsumerGroup(group); + requestHeader.setMaxMsgNums(100); + requestHeader.setQueueId(1); + requestHeader.setQueueOffset(456L); + requestHeader.setSubscription("*"); + requestHeader.setTopic(topic); + requestHeader.setSysFlag(0); + requestHeader.setSubVersion(100L); + RemotingCommand request = RemotingCommand.createRequestCommand(requestCode, requestHeader); + request.makeCustomHeaderToNet(); + return request; + } + + static ConsumerData createConsumerData(String group, String topic) { + ConsumerData consumerData = new ConsumerData(); + consumerData.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + consumerData.setConsumeType(ConsumeType.CONSUME_PASSIVELY); + consumerData.setGroupName(group); + consumerData.setMessageModel(MessageModel.CLUSTERING); + Set subscriptionDataSet = new HashSet<>(); + SubscriptionData subscriptionData = new SubscriptionData(); + subscriptionData.setTopic(topic); + subscriptionData.setSubString("*"); + subscriptionData.setSubVersion(100L); + subscriptionDataSet.add(subscriptionData); + consumerData.setSubscriptionDataSet(subscriptionDataSet); + return consumerData; + } + + private GetMessageResult createGetMessageResult() { + GetMessageResult getMessageResult = new GetMessageResult(); + getMessageResult.setStatus(GetMessageStatus.FOUND); + getMessageResult.setMinOffset(100); + getMessageResult.setMaxOffset(1024); + getMessageResult.setNextBeginOffset(516); + return getMessageResult; + } +} 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 0000000..67ff748 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/QueryAssignmentProcessorTest.java @@ -0,0 +1,247 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +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 testDoLoadBalance() throws Exception { + Method method = queryAssignmentProcessor.getClass() + .getDeclaredMethod("doLoadBalance", String.class, String.class, String.class, MessageModel.class, + String.class, SetMessageRequestModeRequestBody.class, ChannelHandlerContext.class); + method.setAccessible(true); + + Set mqs1 = (Set) method.invoke( + queryAssignmentProcessor, MixAll.LMQ_PREFIX + topic, group, "127.0.0.1", MessageModel.CLUSTERING, + new AllocateMessageQueueAveragely().getName(), new SetMessageRequestModeRequestBody(), handlerContext); + Set mqs2 = (Set) method.invoke( + queryAssignmentProcessor, MixAll.LMQ_PREFIX + topic, group, "127.0.0.2", MessageModel.CLUSTERING, + new AllocateMessageQueueAveragely().getName(), new SetMessageRequestModeRequestBody(), handlerContext); + + assertThat(mqs1).hasSize(1); + assertThat(mqs2).isEmpty(); + } + + @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/QueryMessageProcessorTest.java b/broker/src/test/java/org/apache/rocketmq/broker/processor/QueryMessageProcessorTest.java new file mode 100644 index 0000000..0fd54df --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/QueryMessageProcessorTest.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.broker.processor; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +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.QueryMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ViewMessageRequestHeader; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.QueryMessageResult; +import org.apache.rocketmq.store.SelectMappedBufferResult; +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 java.util.HashMap; + +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 QueryMessageProcessorTest { + private QueryMessageProcessor queryMessageProcessor; + @Spy + private BrokerController brokerController = new BrokerController(new BrokerConfig(), new NettyServerConfig(), new NettyClientConfig(), new MessageStoreConfig()); + + @Mock + private MessageStore messageStore; + + @Mock + private ChannelHandlerContext handlerContext; + + @Mock + private Channel channel; + + @Mock + private ChannelFuture channelFuture; + + @Before + public void init() { + when(handlerContext.channel()).thenReturn(channel); + queryMessageProcessor = new QueryMessageProcessor(brokerController); + when(brokerController.getMessageStore()).thenReturn(messageStore); + when(channel.writeAndFlush(any())).thenReturn(channelFuture); + } + + @Test + public void testQueryMessage() throws RemotingCommandException { + QueryMessageResult result = new QueryMessageResult(); + result.setIndexLastUpdateTimestamp(100); + result.setIndexLastUpdatePhyoffset(0); + result.addMessage(new SelectMappedBufferResult(0, null, 0, null)); + + when(messageStore.queryMessage(anyString(),anyString(),anyInt(),anyLong(),anyLong())).thenReturn(result); + RemotingCommand request = createQueryMessageRequest("topic", "msgKey", 1, 100, 200,"false"); + request.makeCustomHeaderToNet(); + RemotingCommand response = queryMessageProcessor.processRequest(handlerContext, request); + Assert.assertEquals(response.getCode(), ResponseCode.QUERY_NOT_FOUND); + + result.addMessage(new SelectMappedBufferResult(0, null, 1, null)); + when(messageStore.queryMessage(anyString(),anyString(),anyInt(),anyLong(),anyLong())).thenReturn(result); + response = queryMessageProcessor.processRequest(handlerContext, request); + Assert.assertNull(response); + } + + @Test + public void testViewMessageById() throws RemotingCommandException { + ViewMessageRequestHeader viewMessageRequestHeader = new ViewMessageRequestHeader(); + viewMessageRequestHeader.setTopic("topic"); + viewMessageRequestHeader.setOffset(0L); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.VIEW_MESSAGE_BY_ID, viewMessageRequestHeader); + request.makeCustomHeaderToNet(); + request.setCode(RequestCode.VIEW_MESSAGE_BY_ID); + + when(messageStore.selectOneMessageByOffset(anyLong())).thenReturn(null); + RemotingCommand response = queryMessageProcessor.processRequest(handlerContext, request); + Assert.assertEquals(response.getCode(), ResponseCode.SYSTEM_ERROR); + + when(messageStore.selectOneMessageByOffset(anyLong())).thenReturn(new SelectMappedBufferResult(0, null, 0, null)); + response = queryMessageProcessor.processRequest(handlerContext, request); + Assert.assertNull(response); + } + + private RemotingCommand createQueryMessageRequest(String topic, String key, int maxNum, long beginTimestamp, long endTimestamp,String flag) { + QueryMessageRequestHeader requestHeader = new QueryMessageRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setKey(key); + requestHeader.setMaxNum(maxNum); + requestHeader.setBeginTimestamp(beginTimestamp); + requestHeader.setEndTimestamp(endTimestamp); + + HashMap extFields = new HashMap<>(); + extFields.put(MixAll.UNIQUE_MSG_QUERY_FLAG, flag); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_MESSAGE, requestHeader); + request.setExtFields(extFields); + return request; + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/processor/RecallMessageProcessorTest.java b/broker/src/test/java/org/apache/rocketmq/broker/processor/RecallMessageProcessorTest.java new file mode 100644 index 0000000..d28eb2f --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/RecallMessageProcessorTest.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.processor; + +import io.netty.channel.Channel; +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.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.producer.RecallMessageHandle; +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.RecallMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.RecallMessageResponseHeader; +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.BrokerRole; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.stats.BrokerStatsManager; +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 java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; + +@RunWith(MockitoJUnitRunner.class) +public class RecallMessageProcessorTest { + private static final String TOPIC = "topic"; + private static final String BROKER_NAME = "brokerName"; + + private RecallMessageProcessor recallMessageProcessor; + @Mock + private BrokerConfig brokerConfig; + @Mock + private BrokerController brokerController; + @Mock + private ChannelHandlerContext handlerContext; + @Mock + private MessageStoreConfig messageStoreConfig; + @Mock + private TopicConfigManager topicConfigManager; + @Mock + private MessageStore messageStore; + @Mock + private BrokerStatsManager brokerStatsManager; + @Mock + private Channel channel; + + @Before + public void init() throws IllegalAccessException, NoSuchFieldException { + when(brokerController.getMessageStoreConfig()).thenReturn(messageStoreConfig); + when(brokerController.getTopicConfigManager()).thenReturn(topicConfigManager); + when(brokerController.getMessageStore()).thenReturn(messageStore); + when(brokerController.getBrokerConfig()).thenReturn(brokerConfig); + when(brokerConfig.getBrokerName()).thenReturn(BROKER_NAME); + when(brokerConfig.isRecallMessageEnable()).thenReturn(true); + when(brokerController.getBrokerStatsManager()).thenReturn(brokerStatsManager); + when(handlerContext.channel()).thenReturn(channel); + recallMessageProcessor = new RecallMessageProcessor(brokerController); + } + + @Test + public void testBuildMessage() { + String timestampStr = String.valueOf(System.currentTimeMillis()); + String id = "id"; + RecallMessageHandle.HandleV1 handle = new RecallMessageHandle.HandleV1(TOPIC, "brokerName", timestampStr, id); + MessageExtBrokerInner msg = + recallMessageProcessor.buildMessage(handlerContext, new RecallMessageRequestHeader(), handle); + + Assert.assertEquals(TOPIC, msg.getTopic()); + Map properties = MessageDecoder.string2messageProperties(msg.getPropertiesString()); + Assert.assertEquals(timestampStr, properties.get(MessageConst.PROPERTY_TIMER_DELIVER_MS)); + Assert.assertEquals(id, properties.get(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX)); + Assert.assertEquals(TOPIC + "+" + id, properties.get(MessageConst.PROPERTY_TIMER_DEL_UNIQKEY)); + } + + @Test + public void testHandlePutMessageResult() { + MessageExt message = new MessageExt(); + MessageAccessor.putProperty(message, MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, "id"); + RemotingCommand response = RemotingCommand.createResponseCommand(RecallMessageResponseHeader.class); + recallMessageProcessor.handlePutMessageResult(null, null, response, message, handlerContext, 0L); + Assert.assertEquals(ResponseCode.SYSTEM_ERROR, response.getCode()); + + List okStatus = Arrays.asList(PutMessageStatus.PUT_OK, PutMessageStatus.FLUSH_DISK_TIMEOUT, + PutMessageStatus.FLUSH_SLAVE_TIMEOUT, PutMessageStatus.SLAVE_NOT_AVAILABLE); + + for (PutMessageStatus status : PutMessageStatus.values()) { + PutMessageResult putMessageResult = + new PutMessageResult(status, new AppendMessageResult(AppendMessageStatus.PUT_OK)); + recallMessageProcessor.handlePutMessageResult(putMessageResult, null, response, message, handlerContext, 0L); + if (okStatus.contains(status)) { + Assert.assertEquals(ResponseCode.SUCCESS, response.getCode()); + RecallMessageResponseHeader responseHeader = (RecallMessageResponseHeader) response.readCustomHeader(); + Assert.assertEquals("id", responseHeader.getMsgId()); + } else { + Assert.assertEquals(ResponseCode.SYSTEM_ERROR, response.getCode()); + } + } + } + + @Test + public void testProcessRequest_notEnable() throws RemotingCommandException { + when(brokerConfig.isRecallMessageEnable()).thenReturn(false); + RemotingCommand request = mockRequest(0, TOPIC, TOPIC, "id", BROKER_NAME); + RemotingCommand response = recallMessageProcessor.processRequest(handlerContext, request); + Assert.assertEquals(ResponseCode.NO_PERMISSION, response.getCode()); + } + + @Test + public void testProcessRequest_invalidStatus() throws RemotingCommandException { + RemotingCommand request = mockRequest(0, TOPIC, TOPIC, "id", BROKER_NAME); + RemotingCommand response; + + // role slave + when(messageStoreConfig.getBrokerRole()).thenReturn(BrokerRole.SLAVE); + response = recallMessageProcessor.processRequest(handlerContext, request); + Assert.assertEquals(ResponseCode.SLAVE_NOT_AVAILABLE, response.getCode()); + + // not reach startTimestamp + when(messageStoreConfig.getBrokerRole()).thenReturn(BrokerRole.SYNC_MASTER); + when(messageStore.now()).thenReturn(0L); + when(brokerConfig.getStartAcceptSendRequestTimeStamp()).thenReturn(System.currentTimeMillis()); + response = recallMessageProcessor.processRequest(handlerContext, request); + Assert.assertEquals(ResponseCode.SERVICE_NOT_AVAILABLE, response.getCode()); + } + + @Test + public void testProcessRequest_notWriteable() throws RemotingCommandException { + when(brokerConfig.getBrokerPermission()).thenReturn(4); + when(brokerConfig.isAllowRecallWhenBrokerNotWriteable()).thenReturn(false); + RemotingCommand request = mockRequest(0, TOPIC, TOPIC, "id", BROKER_NAME); + RemotingCommand response = recallMessageProcessor.processRequest(handlerContext, request); + Assert.assertEquals(ResponseCode.SERVICE_NOT_AVAILABLE, response.getCode()); + } + + @Test + public void testProcessRequest_topicNotFound_or_notMatch() throws RemotingCommandException { + when(brokerConfig.getBrokerPermission()).thenReturn(6); + RemotingCommand request; + RemotingCommand response; + + // not found + request = mockRequest(0, TOPIC, TOPIC, "id", BROKER_NAME); + response = recallMessageProcessor.processRequest(handlerContext, request); + Assert.assertEquals(ResponseCode.TOPIC_NOT_EXIST, response.getCode()); + + // not match + when(topicConfigManager.selectTopicConfig(TOPIC)).thenReturn(new TopicConfig(TOPIC)); + request = mockRequest(0, TOPIC, "anotherTopic", "id", BROKER_NAME); + response = recallMessageProcessor.processRequest(handlerContext, request); + Assert.assertEquals(ResponseCode.ILLEGAL_OPERATION, response.getCode()); + } + + @Test + public void testProcessRequest_brokerNameNotMatch() throws RemotingCommandException { + when(brokerConfig.getBrokerPermission()).thenReturn(6); + when(topicConfigManager.selectTopicConfig(TOPIC)).thenReturn(new TopicConfig(TOPIC)); + + RemotingCommand request = mockRequest(0, TOPIC, "anotherTopic", "id", BROKER_NAME + "_other"); + RemotingCommand response = recallMessageProcessor.processRequest(handlerContext, request); + Assert.assertEquals(ResponseCode.ILLEGAL_OPERATION, response.getCode()); + } + + @Test + public void testProcessRequest_timestampInvalid() throws RemotingCommandException { + when(brokerConfig.getBrokerPermission()).thenReturn(6); + when(topicConfigManager.selectTopicConfig(TOPIC)).thenReturn(new TopicConfig(TOPIC)); + RemotingCommand request; + RemotingCommand response; + + // past timestamp + request = mockRequest(0, TOPIC, TOPIC, "id", BROKER_NAME); + response = recallMessageProcessor.processRequest(handlerContext, request); + Assert.assertEquals(ResponseCode.ILLEGAL_OPERATION, response.getCode()); + + // timestamp overflow + when(messageStoreConfig.getTimerMaxDelaySec()).thenReturn(86400); + request = mockRequest(System.currentTimeMillis() + 86400 * 2 * 1000, TOPIC, TOPIC, "id", BROKER_NAME); + response = recallMessageProcessor.processRequest(handlerContext, request); + Assert.assertEquals(ResponseCode.ILLEGAL_OPERATION, response.getCode()); + } + + @Test + public void testProcessRequest_success() throws RemotingCommandException { + when(brokerConfig.getBrokerPermission()).thenReturn(6); + when(topicConfigManager.selectTopicConfig(TOPIC)).thenReturn(new TopicConfig(TOPIC)); + when(messageStoreConfig.getTimerMaxDelaySec()).thenReturn(86400); + when(messageStore.putMessage(any())).thenReturn( + new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK))); + + String msgId = "msgId"; + RemotingCommand request = mockRequest(System.currentTimeMillis() + 90 * 1000, TOPIC, TOPIC, msgId, BROKER_NAME); + RemotingCommand response = recallMessageProcessor.processRequest(handlerContext, request); + RecallMessageResponseHeader responseHeader = (RecallMessageResponseHeader) response.readCustomHeader(); + Assert.assertEquals(ResponseCode.SUCCESS, response.getCode()); + Assert.assertEquals(msgId, responseHeader.getMsgId()); + verify(messageStore, times(1)).putMessage(any()); + } + + private RemotingCommand mockRequest(long timestamp, String requestTopic, String handleTopic, + String msgId, String brokerName) { + String handle = + RecallMessageHandle.HandleV1.buildHandle(handleTopic, brokerName, String.valueOf(timestamp), msgId); + RecallMessageRequestHeader requestHeader = new RecallMessageRequestHeader(); + requestHeader.setProducerGroup("group"); + requestHeader.setTopic(requestTopic); + requestHeader.setRecallHandle(handle); + requestHeader.setBrokerName(brokerName); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.RECALL_MESSAGE, requestHeader); + request.makeCustomHeaderToNet(); + return request; + } +} 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 0000000..266c849 --- /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 new file mode 100644 index 0000000..9da6a96 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/SendMessageProcessorTest.java @@ -0,0 +1,447 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.commons.codec.DecoderException; +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.TopicConfig; +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.producer.RecallMessageHandle; +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.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.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.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.when; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.times; + +@RunWith(MockitoJUnitRunner.class) +public class SendMessageProcessorTest { + private SendMessageProcessor sendMessageProcessor; + @Mock + private ChannelHandlerContext handlerContext; + @Mock + private Channel channel; + @Spy + private BrokerConfig brokerConfig; + @Spy + 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(brokerController.getBrokerConfig()).thenReturn(brokerConfig); + when(messageStore.now()).thenReturn(System.currentTimeMillis()); + 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 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 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() { + @Override + public String hookName() { + return null; + } + + @Override + public void sendMessageBefore(SendMessageContext context) { + sendMessageContext[0] = context; + } + + @Override + public void sendMessageAfter(SendMessageContext context) { + + } + }; + sendMessageHookList.add(sendMessageHook); + sendMessageProcessor.registerSendMessageHook(sendMessageHookList); + assertPutResult(ResponseCode.SUCCESS); + assertThat(sendMessageContext[0]).isNotNull(); + assertThat(sendMessageContext[0].getTopic()).isEqualTo(topic); + assertThat(sendMessageContext[0].getProducerGroup()).isEqualTo(group); + } + + @Test + 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 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 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 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 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_BUSY); + } + + @Test + 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 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 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 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); + final RemotingCommand response = sendMessageProcessor.processRequest(handlerContext, request); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @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); + } + + @Test + public void testAttachRecallHandle_skip() { + MessageExt message = new MessageExt(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.SEND_BATCH_MESSAGE, null); + sendMessageProcessor.attachRecallHandle(request, message, new SendMessageResponseHeader()); + + request = RemotingCommand.createRequestCommand(RequestCode.CONSUMER_SEND_MSG_BACK, null); + sendMessageProcessor.attachRecallHandle(request, message, new SendMessageResponseHeader()); + + request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, null); + sendMessageProcessor.attachRecallHandle(request, message, new SendMessageResponseHeader()); + + request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE_V2, null); + sendMessageProcessor.attachRecallHandle(request, message, new SendMessageResponseHeader()); + + verify(brokerConfig, times(0)).getBrokerName(); + } + + @Test + public void testAttachRecallHandle_doAttach() throws DecoderException { + int[] precisionSet = {100, 200, 500, 1000}; + SendMessageResponseHeader responseHeader = new SendMessageResponseHeader(); + String id = MessageClientIDSetter.createUniqID(); + long timestamp = System.currentTimeMillis(); + + for (int precisionMs : precisionSet) { + long deliverMs = floor(timestamp, precisionMs); + MessageExt message = new MessageExt(); + MessageAccessor.putProperty(message, MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, id); + MessageAccessor.putProperty(message, MessageConst.PROPERTY_TIMER_OUT_MS, String.valueOf(deliverMs)); + MessageAccessor.putProperty(message, MessageConst.PROPERTY_REAL_TOPIC, topic); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE_V2, null); + sendMessageProcessor.attachRecallHandle(request, message, responseHeader); + Assert.assertNotNull(responseHeader.getRecallHandle()); + RecallMessageHandle.HandleV1 v1 = + (RecallMessageHandle.HandleV1) RecallMessageHandle.decodeHandle(responseHeader.getRecallHandle()); + Assert.assertEquals(id, v1.getMessageId()); + Assert.assertEquals(topic, v1.getTopic()); + Assert.assertEquals(deliverMs + 1, Long.parseLong(v1.getTimestampStr())); + Assert.assertEquals(deliverMs, floor(Long.valueOf(v1.getTimestampStr()), precisionMs)); + } + } + + private long floor(long deliverMs, int precisionMs) { + assert precisionMs > 0; + if (deliverMs % precisionMs == 0) { + deliverMs -= precisionMs; + } else { + deliverMs = deliverMs / precisionMs * precisionMs; + } + return deliverMs; + } + + 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(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'}); + request.makeCustomHeaderToNet(); + return request; + } + + private RemotingCommand createSendMsgBackCommand(int requestCode) { + ConsumerSendMsgBackRequestHeader requestHeader = new ConsumerSendMsgBackRequestHeader(); + + requestHeader.setMaxReconsumeTimes(3); + requestHeader.setDelayLevel(4); + requestHeader.setGroup(group); + requestHeader.setOffset(123L); + + RemotingCommand request = RemotingCommand.createRequestCommand(requestCode, requestHeader); + request.makeCustomHeaderToNet(); + 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(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); + 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 0000000..b90fb29 --- /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/slave/SlaveSynchronizeAtomicTest.java b/broker/src/test/java/org/apache/rocketmq/broker/slave/SlaveSynchronizeAtomicTest.java new file mode 100644 index 0000000..75db22e --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/slave/SlaveSynchronizeAtomicTest.java @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.slave; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ThreadLocalRandom; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.loadbalance.MessageRequestModeManager; +import org.apache.rocketmq.broker.out.BrokerOuterAPI; +import org.apache.rocketmq.broker.processor.QueryAssignmentProcessor; +import org.apache.rocketmq.broker.subscription.SubscriptionGroupManager; +import org.apache.rocketmq.broker.topic.TopicConfigManager; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.common.BrokerConfig; +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.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.remoting.protocol.body.MessageRequestModeSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.body.SubscriptionGroupWrapper; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +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; + +@RunWith(MockitoJUnitRunner.class) +public class SlaveSynchronizeAtomicTest { + @Spy + private BrokerController brokerController = + new BrokerController(new BrokerConfig(), new NettyServerConfig(), new NettyClientConfig(), + new MessageStoreConfig()); + + private SlaveSynchronize slaveSynchronize; + + @Mock + private BrokerOuterAPI brokerOuterAPI; + + @Mock + private TopicConfigManager topicConfigManager; + + + @Mock + private SubscriptionGroupManager subscriptionGroupManager; + + @Mock + private QueryAssignmentProcessor queryAssignmentProcessor; + + @Mock + private MessageRequestModeManager messageRequestModeManager; + + + private static final String BROKER_ADDR = "127.0.0.1:10911"; + private final SubscriptionGroupWrapper subscriptionGroupWrapper = createSubscriptionGroupWrapper(); + private final MessageRequestModeSerializeWrapper requestModeSerializeWrapper = createMessageRequestModeWrapper(); + private final DataVersion dataVersion = new DataVersion(); + + @Before + public void init() { + for (int i = 0; i < 100000; i++) { + subscriptionGroupWrapper.getSubscriptionGroupTable().put("group" + i, new SubscriptionGroupConfig()); + } + for (int i = 0; i < 100000; i++) { + requestModeSerializeWrapper.getMessageRequestModeMap().put("topic" + i, new ConcurrentHashMap<>()); + } + when(brokerController.getBrokerOuterAPI()).thenReturn(brokerOuterAPI); + when(brokerController.getTopicConfigManager()).thenReturn(topicConfigManager); + when(brokerController.getSubscriptionGroupManager()).thenReturn(subscriptionGroupManager); + when(subscriptionGroupManager.getDataVersion()).thenReturn(dataVersion); + when(subscriptionGroupManager.getSubscriptionGroupTable()).thenReturn( + subscriptionGroupWrapper.getSubscriptionGroupTable()); + slaveSynchronize = new SlaveSynchronize(brokerController); + slaveSynchronize.setMasterAddr(BROKER_ADDR); + } + + private SubscriptionGroupWrapper createSubscriptionGroupWrapper() { + SubscriptionGroupWrapper wrapper = new SubscriptionGroupWrapper(); + wrapper.setSubscriptionGroupTable(new ConcurrentHashMap<>()); + DataVersion dataVersion = new DataVersion(); + dataVersion.setStateVersion(1L); + wrapper.setDataVersion(dataVersion); + return wrapper; + } + + private MessageRequestModeSerializeWrapper createMessageRequestModeWrapper() { + MessageRequestModeSerializeWrapper wrapper = new MessageRequestModeSerializeWrapper(); + wrapper.setMessageRequestModeMap(new ConcurrentHashMap<>()); + return wrapper; + } + + @Test + public void testSyncAtomically() + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, + InterruptedException { + when(brokerOuterAPI.getAllSubscriptionGroupConfig(anyString())).thenReturn(subscriptionGroupWrapper); + when(brokerOuterAPI.getAllMessageRequestMode(anyString())).thenReturn(requestModeSerializeWrapper); + + CountDownLatch countDownLatch = new CountDownLatch(1); + new Thread(() -> { + while (countDownLatch.getCount() > 0) { + dataVersion.nextVersion(); + try { + slaveSynchronize.syncAll(); + } catch (Exception e) { + e.printStackTrace(); + } + } + }).start(); + + for (int i = 0; i < 10000000; i++) { + Assert.assertTrue(subscriptionGroupWrapper.getSubscriptionGroupTable() + .containsKey("group" + ThreadLocalRandom.current().nextInt(0, 100000))); + Assert.assertTrue(requestModeSerializeWrapper.getMessageRequestModeMap() + .containsKey("topic" + ThreadLocalRandom.current().nextInt(0, 100000))); + } + countDownLatch.countDown(); + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/slave/SlaveSynchronizeTest.java b/broker/src/test/java/org/apache/rocketmq/broker/slave/SlaveSynchronizeTest.java new file mode 100644 index 0000000..95db733 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/slave/SlaveSynchronizeTest.java @@ -0,0 +1,206 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.slave; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.loadbalance.MessageRequestModeManager; +import org.apache.rocketmq.broker.offset.ConsumerOffsetManager; +import org.apache.rocketmq.broker.out.BrokerOuterAPI; +import org.apache.rocketmq.broker.processor.QueryAssignmentProcessor; +import org.apache.rocketmq.broker.schedule.ScheduleMessageService; +import org.apache.rocketmq.broker.subscription.SubscriptionGroupManager; +import org.apache.rocketmq.broker.topic.TopicConfigManager; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.TopicConfig; +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.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.DataVersion; +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.MessageStore; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.timer.TimerCheckpoint; +import org.apache.rocketmq.store.timer.TimerMessageStore; +import org.apache.rocketmq.store.timer.TimerMetrics; +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 java.io.UnsupportedEncodingException; +import java.util.concurrent.ConcurrentHashMap; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class SlaveSynchronizeTest { + @Spy + private BrokerController brokerController = new BrokerController(new BrokerConfig(), new NettyServerConfig(), new NettyClientConfig(), new MessageStoreConfig()); + + private SlaveSynchronize slaveSynchronize; + + @Mock + private BrokerOuterAPI brokerOuterAPI; + + @Mock + private TopicConfigManager topicConfigManager; + + @Mock + private ConsumerOffsetManager consumerOffsetManager; + + @Mock + private MessageStoreConfig messageStoreConfig; + + @Mock + private MessageStore messageStore; + + @Mock + private ScheduleMessageService scheduleMessageService; + + @Mock + private SubscriptionGroupManager subscriptionGroupManager; + + @Mock + private QueryAssignmentProcessor queryAssignmentProcessor; + + @Mock + private MessageRequestModeManager messageRequestModeManager; + + @Mock + private TimerMessageStore timerMessageStore; + + @Mock + private TimerMetrics timerMetrics; + + @Mock + private TimerCheckpoint timerCheckpoint; + + private static final String BROKER_ADDR = "127.0.0.1:10911"; + + @Before + public void init() { + when(brokerController.getBrokerOuterAPI()).thenReturn(brokerOuterAPI); + when(brokerController.getTopicConfigManager()).thenReturn(topicConfigManager); + when(brokerController.getMessageStoreConfig()).thenReturn(messageStoreConfig); + when(brokerController.getScheduleMessageService()).thenReturn(scheduleMessageService); + when(brokerController.getSubscriptionGroupManager()).thenReturn(subscriptionGroupManager); + when(brokerController.getQueryAssignmentProcessor()).thenReturn(queryAssignmentProcessor); + when(brokerController.getMessageStore()).thenReturn(messageStore); + when(brokerController.getTimerMessageStore()).thenReturn(timerMessageStore); + when(brokerController.getTimerCheckpoint()).thenReturn(timerCheckpoint); + when(topicConfigManager.getDataVersion()).thenReturn(new DataVersion()); + when(topicConfigManager.getTopicConfigTable()).thenReturn(new ConcurrentHashMap<>()); + when(brokerController.getConsumerOffsetManager()).thenReturn(consumerOffsetManager); + when(consumerOffsetManager.getOffsetTable()).thenReturn(new ConcurrentHashMap<>()); + when(consumerOffsetManager.getDataVersion()).thenReturn(new DataVersion()); + when(subscriptionGroupManager.getDataVersion()).thenReturn(new DataVersion()); + when(subscriptionGroupManager.getSubscriptionGroupTable()).thenReturn(new ConcurrentHashMap<>()); + when(queryAssignmentProcessor.getMessageRequestModeManager()).thenReturn(messageRequestModeManager); + when(messageRequestModeManager.getMessageRequestModeMap()).thenReturn(new ConcurrentHashMap<>()); + when(messageStoreConfig.isTimerWheelEnable()).thenReturn(true); + when(messageStore.getTimerMessageStore()).thenReturn(timerMessageStore); + when(timerMessageStore.isShouldRunningDequeue()).thenReturn(false); + when(timerMessageStore.getTimerMetrics()).thenReturn(timerMetrics); + when(timerMetrics.getDataVersion()).thenReturn(new DataVersion()); + when(timerCheckpoint.getDataVersion()).thenReturn(new DataVersion()); + slaveSynchronize = new SlaveSynchronize(brokerController); + slaveSynchronize.setMasterAddr(BROKER_ADDR); + } + + @Test + public void testSyncAll() throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException, UnsupportedEncodingException { + TopicConfig newTopicConfig = new TopicConfig("NewTopic"); + when(brokerOuterAPI.getAllTopicConfig(anyString())).thenReturn(createTopicConfigWrapper(newTopicConfig)); + when(brokerOuterAPI.getAllConsumerOffset(anyString())).thenReturn(createConsumerOffsetWrapper()); + when(brokerOuterAPI.getAllDelayOffset(anyString())).thenReturn(""); + when(brokerOuterAPI.getAllSubscriptionGroupConfig(anyString())).thenReturn(createSubscriptionGroupWrapper()); + when(brokerOuterAPI.getAllMessageRequestMode(anyString())).thenReturn(createMessageRequestModeWrapper()); + when(brokerOuterAPI.getTimerMetrics(anyString())).thenReturn(createTimerMetricsWrapper()); + slaveSynchronize.syncAll(); + Assert.assertEquals(1, this.brokerController.getTopicConfigManager().getDataVersion().getStateVersion()); + Assert.assertEquals(1, this.brokerController.getTopicQueueMappingManager().getDataVersion().getStateVersion()); + Assert.assertEquals(1, consumerOffsetManager.getDataVersion().getStateVersion()); + Assert.assertEquals(1, subscriptionGroupManager.getDataVersion().getStateVersion()); + Assert.assertEquals(1, timerMetrics.getDataVersion().getStateVersion()); + } + + @Test + public void testGetMasterAddr() { + Assert.assertEquals(BROKER_ADDR, slaveSynchronize.getMasterAddr()); + } + + @Test + public void testSyncTimerCheckPoint() throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + when(brokerOuterAPI.getTimerCheckPoint(anyString())).thenReturn(timerCheckpoint); + slaveSynchronize.syncTimerCheckPoint(); + Assert.assertEquals(0, timerCheckpoint.getDataVersion().getStateVersion()); + } + + private TopicConfigAndMappingSerializeWrapper createTopicConfigWrapper(TopicConfig topicConfig) { + TopicConfigAndMappingSerializeWrapper wrapper = new TopicConfigAndMappingSerializeWrapper(); + wrapper.setTopicConfigTable(new ConcurrentHashMap<>()); + wrapper.getTopicConfigTable().put(topicConfig.getTopicName(), topicConfig); + DataVersion dataVersion = new DataVersion(); + dataVersion.setStateVersion(1L); + wrapper.setDataVersion(dataVersion); + wrapper.setMappingDataVersion(dataVersion); + return wrapper; + } + + private ConsumerOffsetSerializeWrapper createConsumerOffsetWrapper() { + ConsumerOffsetSerializeWrapper wrapper = new ConsumerOffsetSerializeWrapper(); + wrapper.setOffsetTable(new ConcurrentHashMap<>()); + DataVersion dataVersion = new DataVersion(); + dataVersion.setStateVersion(1L); + wrapper.setDataVersion(dataVersion); + return wrapper; + } + + private SubscriptionGroupWrapper createSubscriptionGroupWrapper() { + SubscriptionGroupWrapper wrapper = new SubscriptionGroupWrapper(); + wrapper.setSubscriptionGroupTable(new ConcurrentHashMap<>()); + DataVersion dataVersion = new DataVersion(); + dataVersion.setStateVersion(1L); + wrapper.setDataVersion(dataVersion); + return wrapper; + } + + private MessageRequestModeSerializeWrapper createMessageRequestModeWrapper() { + MessageRequestModeSerializeWrapper wrapper = new MessageRequestModeSerializeWrapper(); + wrapper.setMessageRequestModeMap(new ConcurrentHashMap<>()); + return wrapper; + } + + private TimerMetrics.TimerMetricsSerializeWrapper createTimerMetricsWrapper() { + TimerMetrics.TimerMetricsSerializeWrapper wrapper = new TimerMetrics.TimerMetricsSerializeWrapper(); + wrapper.setTimingCount(new ConcurrentHashMap<>()); + DataVersion dataVersion = new DataVersion(); + dataVersion.setStateVersion(1L); + wrapper.setDataVersion(dataVersion); + return wrapper; + } +} 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 0000000..bdaee3b --- /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/RocksdbGroupConfigTransferTest.java b/broker/src/test/java/org/apache/rocketmq/broker/subscription/RocksdbGroupConfigTransferTest.java new file mode 100644 index 0000000..c75fe0d --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/subscription/RocksdbGroupConfigTransferTest.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.broker.subscription; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.config.v1.RocksDBSubscriptionGroupManager; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +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.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class RocksdbGroupConfigTransferTest { + private final String basePath = Paths.get(System.getProperty("user.home"), + "unit-test-store", UUID.randomUUID().toString().substring(0, 16).toUpperCase()).toString(); + + private RocksDBSubscriptionGroupManager rocksDBSubscriptionGroupManager; + + private SubscriptionGroupManager jsonSubscriptionGroupManager; + @Mock + private BrokerController brokerController; + + @Mock + private DefaultMessageStore defaultMessageStore; + + @Before + public void init() { + if (notToBeExecuted()) { + return; + } + BrokerConfig brokerConfig = new BrokerConfig(); + Mockito.lenient().when(brokerController.getBrokerConfig()).thenReturn(brokerConfig); + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setStorePathRootDir(basePath); + Mockito.lenient().when(brokerController.getMessageStoreConfig()).thenReturn(messageStoreConfig); + Mockito.lenient().when(brokerController.getMessageStore()).thenReturn(defaultMessageStore); + when(defaultMessageStore.getStateMachineVersion()).thenReturn(0L); + } + + @After + public void destroy() { + if (notToBeExecuted()) { + return; + } + Path pathToBeDeleted = Paths.get(basePath); + + try { + Files.walk(pathToBeDeleted) + .sorted(Comparator.reverseOrder()) + .forEach(path -> { + try { + Files.delete(path); + } catch (IOException e) { + // ignore + } + }); + } catch (IOException e) { + // ignore + } + if (rocksDBSubscriptionGroupManager != null) { + rocksDBSubscriptionGroupManager.stop(); + } + } + + + public void initRocksDBSubscriptionGroupManager() { + if (rocksDBSubscriptionGroupManager == null) { + rocksDBSubscriptionGroupManager = new RocksDBSubscriptionGroupManager(brokerController); + rocksDBSubscriptionGroupManager.load(); + } + } + + public void initJsonSubscriptionGroupManager() { + if (jsonSubscriptionGroupManager == null) { + jsonSubscriptionGroupManager = new SubscriptionGroupManager(brokerController); + jsonSubscriptionGroupManager.load(); + } + } + + @Test + public void theFirstTimeLoadJsonSubscriptionGroupManager() { + if (notToBeExecuted()) { + return; + } + initJsonSubscriptionGroupManager(); + DataVersion dataVersion = jsonSubscriptionGroupManager.getDataVersion(); + Assert.assertNotNull(dataVersion); + Assert.assertEquals(0L, dataVersion.getCounter().get()); + Assert.assertEquals(0L, dataVersion.getStateVersion()); + Assert.assertNotEquals(0, jsonSubscriptionGroupManager.getSubscriptionGroupTable().size()); + } + + @Test + public void theFirstTimeLoadRocksDBSubscriptionGroupManager() { + if (notToBeExecuted()) { + return; + } + initRocksDBSubscriptionGroupManager(); + DataVersion dataVersion = rocksDBSubscriptionGroupManager.getDataVersion(); + Assert.assertNotNull(dataVersion); + Assert.assertEquals(0L, dataVersion.getCounter().get()); + Assert.assertEquals(0L, dataVersion.getStateVersion()); + Assert.assertNotEquals(0, rocksDBSubscriptionGroupManager.getSubscriptionGroupTable().size()); + } + + + @Test + public void addGroupLoadJsonSubscriptionGroupManager() { + if (notToBeExecuted()) { + return; + } + initJsonSubscriptionGroupManager(); + int beforeSize = jsonSubscriptionGroupManager.getSubscriptionGroupTable().size(); + String groupName = "testAddGroupConfig-" + System.currentTimeMillis(); + + Map attributes = new HashMap<>(); + + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setGroupName(groupName); + subscriptionGroupConfig.setAttributes(attributes); + DataVersion beforeDataVersion = jsonSubscriptionGroupManager.getDataVersion(); + long beforeDataVersionCounter = beforeDataVersion.getCounter().get(); + long beforeTimestamp = beforeDataVersion.getTimestamp(); + + jsonSubscriptionGroupManager.updateSubscriptionGroupConfig(subscriptionGroupConfig); + + int afterSize = jsonSubscriptionGroupManager.getSubscriptionGroupTable().size(); + DataVersion afterDataVersion = jsonSubscriptionGroupManager.getDataVersion(); + long afterDataVersionCounter = afterDataVersion.getCounter().get(); + long afterTimestamp = afterDataVersion.getTimestamp(); + + Assert.assertEquals(0, beforeDataVersionCounter); + Assert.assertEquals(1, afterDataVersionCounter); + Assert.assertEquals(1, afterSize - beforeSize); + Assert.assertTrue(afterTimestamp >= beforeTimestamp); + } + + @Test + public void addForbiddenGroupLoadJsonSubscriptionGroupManager() { + if (notToBeExecuted()) { + return; + } + initJsonSubscriptionGroupManager(); + int beforeSize = jsonSubscriptionGroupManager.getForbiddenTable().size(); + String groupName = "testAddGroupConfig-" + System.currentTimeMillis(); + + Map attributes = new HashMap<>(); + + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setGroupName(groupName); + subscriptionGroupConfig.setAttributes(attributes); + DataVersion beforeDataVersion = jsonSubscriptionGroupManager.getDataVersion(); + long beforeDataVersionCounter = beforeDataVersion.getCounter().get(); + long beforeTimestamp = beforeDataVersion.getTimestamp(); + + jsonSubscriptionGroupManager.setForbidden(groupName, "topic", 0); + int afterSize = jsonSubscriptionGroupManager.getForbiddenTable().size(); + DataVersion afterDataVersion = jsonSubscriptionGroupManager.getDataVersion(); + long afterDataVersionCounter = afterDataVersion.getCounter().get(); + long afterTimestamp = afterDataVersion.getTimestamp(); + + Assert.assertEquals(1, afterDataVersionCounter - beforeDataVersionCounter); + Assert.assertEquals(1, afterSize - beforeSize); + Assert.assertTrue(afterTimestamp >= beforeTimestamp); + } + + @Test + public void addGroupLoadRocksdbSubscriptionGroupManager() { + if (notToBeExecuted()) { + return; + } + initRocksDBSubscriptionGroupManager(); + int beforeSize = rocksDBSubscriptionGroupManager.getSubscriptionGroupTable().size(); + String groupName = "testAddGroupConfig-" + System.currentTimeMillis(); + + Map attributes = new HashMap<>(); + + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setGroupName(groupName); + subscriptionGroupConfig.setAttributes(attributes); + DataVersion beforeDataVersion = rocksDBSubscriptionGroupManager.getDataVersion(); + long beforeDataVersionCounter = beforeDataVersion.getCounter().get(); + long beforeTimestamp = beforeDataVersion.getTimestamp(); + + rocksDBSubscriptionGroupManager.updateSubscriptionGroupConfig(subscriptionGroupConfig); + int afterSize = rocksDBSubscriptionGroupManager.getSubscriptionGroupTable().size(); + DataVersion afterDataVersion = rocksDBSubscriptionGroupManager.getDataVersion(); + long afterDataVersionCounter = afterDataVersion.getCounter().get(); + long afterTimestamp = afterDataVersion.getTimestamp(); + Assert.assertEquals(1, afterDataVersionCounter); + Assert.assertEquals(0, beforeDataVersionCounter); + Assert.assertEquals(1, afterSize - beforeSize); + Assert.assertTrue(afterTimestamp >= beforeTimestamp); + } + + @Test + public void addForbiddenLoadRocksdbSubscriptionGroupManager() { + if (notToBeExecuted()) { + return; + } + initRocksDBSubscriptionGroupManager(); + int beforeSize = rocksDBSubscriptionGroupManager.getForbiddenTable().size(); + String groupName = "testAddGroupConfig-" + System.currentTimeMillis(); + + Map attributes = new HashMap<>(); + + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setGroupName(groupName); + subscriptionGroupConfig.setAttributes(attributes); + DataVersion beforeDataVersion = rocksDBSubscriptionGroupManager.getDataVersion(); + long beforeDataVersionCounter = beforeDataVersion.getCounter().get(); + long beforeTimestamp = beforeDataVersion.getTimestamp(); + + rocksDBSubscriptionGroupManager.updateForbidden(groupName, "topic", 0, true); + + int afterSize = rocksDBSubscriptionGroupManager.getForbiddenTable().size(); + DataVersion afterDataVersion = rocksDBSubscriptionGroupManager.getDataVersion(); + long afterDataVersionCounter = afterDataVersion.getCounter().get(); + long afterTimestamp = afterDataVersion.getTimestamp(); + Assert.assertEquals(1, afterDataVersionCounter - beforeDataVersionCounter); + Assert.assertEquals(1, afterSize - beforeSize); + Assert.assertTrue(afterTimestamp >= beforeTimestamp); + Assert.assertNotEquals(0, rocksDBSubscriptionGroupManager.getSubscriptionGroupTable().size()); + } + + @Test + public void theSecondTimeLoadJsonSubscriptionGroupManager() { + if (notToBeExecuted()) { + return; + } + addGroupLoadJsonSubscriptionGroupManager(); + jsonSubscriptionGroupManager.stop(); + rocksDBSubscriptionGroupManager = null; + addForbiddenGroupLoadJsonSubscriptionGroupManager(); + jsonSubscriptionGroupManager.stop(); + rocksDBSubscriptionGroupManager = null; + jsonSubscriptionGroupManager = new SubscriptionGroupManager(brokerController); + jsonSubscriptionGroupManager.load(); + DataVersion dataVersion = jsonSubscriptionGroupManager.getDataVersion(); + Assert.assertNotNull(dataVersion); + Assert.assertEquals(2L, dataVersion.getCounter().get()); + Assert.assertEquals(0L, dataVersion.getStateVersion()); + Assert.assertNotEquals(0, jsonSubscriptionGroupManager.getSubscriptionGroupTable().size()); + Assert.assertNotEquals(0, jsonSubscriptionGroupManager.getForbiddenTable().size()); + Assert.assertNotEquals(0, jsonSubscriptionGroupManager.getSubscriptionGroupTable().size()); + } + + @Test + public void theSecondTimeLoadRocksdbTopicConfigManager() { + if (notToBeExecuted()) { + return; + } + addGroupLoadRocksdbSubscriptionGroupManager(); + rocksDBSubscriptionGroupManager.stop(); + rocksDBSubscriptionGroupManager = null; + addForbiddenLoadRocksdbSubscriptionGroupManager(); + rocksDBSubscriptionGroupManager.stop(); + rocksDBSubscriptionGroupManager = null; + rocksDBSubscriptionGroupManager = new RocksDBSubscriptionGroupManager(brokerController); + rocksDBSubscriptionGroupManager.load(); + DataVersion dataVersion = rocksDBSubscriptionGroupManager.getDataVersion(); + Assert.assertNotNull(dataVersion); + Assert.assertEquals(2L, dataVersion.getCounter().get()); + Assert.assertEquals(0L, dataVersion.getStateVersion()); + Assert.assertNotEquals(0, rocksDBSubscriptionGroupManager.getSubscriptionGroupTable().size()); + Assert.assertNotEquals(0, rocksDBSubscriptionGroupManager.getForbiddenTable().size()); + Assert.assertNotEquals(0, rocksDBSubscriptionGroupManager.getSubscriptionGroupTable().size()); + } + + + @Test + public void jsonUpgradeToRocksdb() { + if (notToBeExecuted()) { + return; + } + addGroupLoadJsonSubscriptionGroupManager(); + addForbiddenGroupLoadJsonSubscriptionGroupManager(); + initRocksDBSubscriptionGroupManager(); + DataVersion dataVersion = rocksDBSubscriptionGroupManager.getDataVersion(); + Assert.assertNotNull(dataVersion); + Assert.assertEquals(3L, dataVersion.getCounter().get()); + Assert.assertEquals(0L, dataVersion.getStateVersion()); + Assert.assertNotEquals(0, rocksDBSubscriptionGroupManager.getForbiddenTable().size()); + Assert.assertNotEquals(0, rocksDBSubscriptionGroupManager.getSubscriptionGroupTable().size()); + Assert.assertEquals(rocksDBSubscriptionGroupManager.getSubscriptionGroupTable().size(), jsonSubscriptionGroupManager.getSubscriptionGroupTable().size()); + Assert.assertEquals(rocksDBSubscriptionGroupManager.getForbiddenTable().size(), jsonSubscriptionGroupManager.getForbiddenTable().size()); + + rocksDBSubscriptionGroupManager.stop(); + rocksDBSubscriptionGroupManager = new RocksDBSubscriptionGroupManager(brokerController); + rocksDBSubscriptionGroupManager.load(); + dataVersion = rocksDBSubscriptionGroupManager.getDataVersion(); + Assert.assertEquals(3L, dataVersion.getCounter().get()); + Assert.assertEquals(0L, dataVersion.getStateVersion()); + Assert.assertNotEquals(0, rocksDBSubscriptionGroupManager.getForbiddenTable().size()); + Assert.assertNotEquals(0, rocksDBSubscriptionGroupManager.getSubscriptionGroupTable().size()); + Assert.assertEquals(rocksDBSubscriptionGroupManager.getSubscriptionGroupTable().size(), jsonSubscriptionGroupManager.getSubscriptionGroupTable().size()); + Assert.assertEquals(rocksDBSubscriptionGroupManager.getForbiddenTable().size(), jsonSubscriptionGroupManager.getForbiddenTable().size()); + } + + private boolean notToBeExecuted() { + return MixAll.isMac(); + } + +} 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 0000000..3384d47 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/subscription/SubscriptionGroupManagerTest.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.broker.subscription; + +import com.google.common.collect.ImmutableMap; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +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.Mockito; +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.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + + +@RunWith(MockitoJUnitRunner.class) +public class SubscriptionGroupManagerTest { + private String group = "group"; + + private final String basePath = Paths.get(System.getProperty("user.home"), + "unit-test-store", UUID.randomUUID().toString().substring(0, 16).toUpperCase()).toString(); + @Mock + private BrokerController brokerControllerMock; + private SubscriptionGroupManager subscriptionGroupManager; + + @Before + public void before() { + if (notToBeExecuted()) { + return; + } + SubscriptionGroupAttributes.ALL.put("test", new BooleanAttribute( + "test", + false, + false + )); + subscriptionGroupManager = spy(new SubscriptionGroupManager(brokerControllerMock)); + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setStorePathRootDir(basePath); + Mockito.lenient().when(brokerControllerMock.getMessageStoreConfig()).thenReturn(messageStoreConfig); + } + + @After + public void destroy() { + if (notToBeExecuted()) { + return; + } + if (subscriptionGroupManager != null) { + subscriptionGroupManager.stop(); + } + } + + @Test + public void testUpdateAndCreateSubscriptionGroupInRocksdb() { + if (notToBeExecuted()) { + return; + } + group += System.currentTimeMillis(); + updateSubscriptionGroupConfig(); + } + + @Test + public void updateSubscriptionGroupConfig() { + if (notToBeExecuted()) { + return; + } + 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"); + } + + private boolean notToBeExecuted() { + return MixAll.isMac(); + } + @Test + public void testUpdateSubscriptionGroupConfigList_NullConfigList() { + if (notToBeExecuted()) { + return; + } + + subscriptionGroupManager.updateSubscriptionGroupConfigList(null); + // Verifying that persist() is not called + verify(subscriptionGroupManager, never()).persist(); + } + + @Test + public void testUpdateSubscriptionGroupConfigList_EmptyConfigList() { + if (notToBeExecuted()) { + return; + } + + subscriptionGroupManager.updateSubscriptionGroupConfigList(Collections.emptyList()); + // Verifying that persist() is not called + verify(subscriptionGroupManager, never()).persist(); + } + + @Test + public void testUpdateSubscriptionGroupConfigList_ValidConfigList() { + if (notToBeExecuted()) { + return; + } + + final List configList = new LinkedList<>(); + final List groupNames = new LinkedList<>(); + for (int i = 0; i < 10; i++) { + SubscriptionGroupConfig config = new SubscriptionGroupConfig(); + String groupName = String.format("group-%d", i); + config.setGroupName(groupName); + configList.add(config); + groupNames.add(groupName); + } + + subscriptionGroupManager.updateSubscriptionGroupConfigList(configList); + + // Verifying that persist() is called once + verify(subscriptionGroupManager, times(1)).persist(); + + groupNames.forEach(groupName -> + assertThat(subscriptionGroupManager.getSubscriptionGroupTable().get(groupName)).isNotNull()); + + } + +} \ 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 0000000..fa3ef95 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/topic/RocksdbTopicConfigManagerTest.java @@ -0,0 +1,384 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Paths; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.config.v1.RocksDBTopicConfigManager; +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.Mockito; +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 final String basePath = Paths.get(System.getProperty("user.home"), + "unit-test-store", UUID.randomUUID().toString().substring(0, 16).toUpperCase()).toString(); + + 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(); + messageStoreConfig.setStorePathRootDir(basePath); + when(brokerController.getMessageStoreConfig()).thenReturn(messageStoreConfig); + Mockito.lenient().when(brokerController.getMessageStore()).thenReturn(defaultMessageStore); + Mockito.lenient().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")); + } + + @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/RocksdbTopicConfigTransferTest.java b/broker/src/test/java/org/apache/rocketmq/broker/topic/RocksdbTopicConfigTransferTest.java new file mode 100644 index 0000000..e925ed4 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/topic/RocksdbTopicConfigTransferTest.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.topic; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.config.v1.RocksDBTopicConfigManager; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.remoting.protocol.DataVersion; +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.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class RocksdbTopicConfigTransferTest { + + private final String basePath = Paths.get(System.getProperty("user.home"), + "unit-test-store", UUID.randomUUID().toString().substring(0, 16).toUpperCase()).toString(); + + private RocksDBTopicConfigManager rocksdbTopicConfigManager; + + private TopicConfigManager jsonTopicConfigManager; + @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(); + messageStoreConfig.setStorePathRootDir(basePath); + Mockito.lenient().when(brokerController.getMessageStoreConfig()).thenReturn(messageStoreConfig); + when(brokerController.getMessageStore()).thenReturn(defaultMessageStore); + when(defaultMessageStore.getStateMachineVersion()).thenReturn(0L); + } + + @After + public void destroy() { + if (notToBeExecuted()) { + return; + } + Path pathToBeDeleted = Paths.get(basePath); + try { + Files.walk(pathToBeDeleted) + .sorted(Comparator.reverseOrder()) + .forEach(path -> { + try { + Files.delete(path); + } catch (IOException e) { + // ignore + } + }); + } catch (IOException e) { + // ignore + } + if (rocksdbTopicConfigManager != null) { + rocksdbTopicConfigManager.stop(); + } + } + + public void initRocksdbTopicConfigManager() { + if (rocksdbTopicConfigManager == null) { + rocksdbTopicConfigManager = new RocksDBTopicConfigManager(brokerController); + rocksdbTopicConfigManager.load(); + } + } + + public void initJsonTopicConfigManager() { + if (jsonTopicConfigManager == null) { + jsonTopicConfigManager = new TopicConfigManager(brokerController); + jsonTopicConfigManager.load(); + } + } + + @Test + public void theFirstTimeLoadJsonTopicConfigManager() { + if (notToBeExecuted()) { + return; + } + initJsonTopicConfigManager(); + DataVersion dataVersion = jsonTopicConfigManager.getDataVersion(); + Assert.assertNotNull(dataVersion); + Assert.assertEquals(0L, dataVersion.getCounter().get()); + Assert.assertEquals(0L, dataVersion.getStateVersion()); + Assert.assertNotEquals(0, jsonTopicConfigManager.getTopicConfigTable().size()); + } + + @Test + public void theFirstTimeLoadRocksdbTopicConfigManager() { + if (notToBeExecuted()) { + return; + } + initRocksdbTopicConfigManager(); + DataVersion dataVersion = rocksdbTopicConfigManager.getDataVersion(); + Assert.assertNotNull(dataVersion); + Assert.assertEquals(0L, dataVersion.getCounter().get()); + Assert.assertEquals(0L, dataVersion.getStateVersion()); + Assert.assertNotEquals(0, rocksdbTopicConfigManager.getTopicConfigTable().size()); + } + + + @Test + public void addTopicLoadJsonTopicConfigManager() { + if (notToBeExecuted()) { + return; + } + initJsonTopicConfigManager(); + String topicName = "testAddTopicConfig-" + System.currentTimeMillis(); + + Map attributes = new HashMap<>(); + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName(topicName); + topicConfig.setAttributes(attributes); + DataVersion beforeDataVersion = jsonTopicConfigManager.getDataVersion(); + long beforeDataVersionCounter = beforeDataVersion.getCounter().get(); + long beforeTimestamp = beforeDataVersion.getTimestamp(); + + jsonTopicConfigManager.updateTopicConfig(topicConfig); + + DataVersion afterDataVersion = jsonTopicConfigManager.getDataVersion(); + long afterDataVersionCounter = afterDataVersion.getCounter().get(); + long afterTimestamp = afterDataVersion.getTimestamp(); + + Assert.assertEquals(0, beforeDataVersionCounter); + Assert.assertEquals(1, afterDataVersionCounter); + Assert.assertTrue(afterTimestamp >= beforeTimestamp); + } + + @Test + public void addTopicLoadRocksdbTopicConfigManager() { + if (notToBeExecuted()) { + return; + } + initRocksdbTopicConfigManager(); + String topicName = "testAddTopicConfig-" + System.currentTimeMillis(); + + Map attributes = new HashMap<>(); + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName(topicName); + topicConfig.setAttributes(attributes); + DataVersion beforeDataVersion = rocksdbTopicConfigManager.getDataVersion(); + long beforeDataVersionCounter = beforeDataVersion.getCounter().get(); + long beforeTimestamp = beforeDataVersion.getTimestamp(); + + rocksdbTopicConfigManager.updateTopicConfig(topicConfig); + + DataVersion afterDataVersion = rocksdbTopicConfigManager.getDataVersion(); + long afterDataVersionCounter = afterDataVersion.getCounter().get(); + long afterTimestamp = afterDataVersion.getTimestamp(); + Assert.assertEquals(0, beforeDataVersionCounter); + Assert.assertEquals(1, afterDataVersionCounter); + Assert.assertTrue(afterTimestamp >= beforeTimestamp); + } + + @Test + public void theSecondTimeLoadJsonTopicConfigManager() { + if (notToBeExecuted()) { + return; + } + addTopicLoadJsonTopicConfigManager(); + jsonTopicConfigManager.stop(); + jsonTopicConfigManager = new TopicConfigManager(brokerController); + jsonTopicConfigManager.load(); + DataVersion dataVersion = jsonTopicConfigManager.getDataVersion(); + Assert.assertNotNull(dataVersion); + Assert.assertEquals(1L, dataVersion.getCounter().get()); + Assert.assertEquals(0L, dataVersion.getStateVersion()); + Assert.assertNotEquals(0, jsonTopicConfigManager.getTopicConfigTable().size()); + } + + @Test + public void theSecondTimeLoadRocksdbTopicConfigManager() { + if (notToBeExecuted()) { + return; + } + addTopicLoadRocksdbTopicConfigManager(); + rocksdbTopicConfigManager.stop(); + rocksdbTopicConfigManager = null; + rocksdbTopicConfigManager = new RocksDBTopicConfigManager(brokerController); + rocksdbTopicConfigManager.load(); + DataVersion dataVersion = rocksdbTopicConfigManager.getDataVersion(); + Assert.assertNotNull(dataVersion); + Assert.assertEquals(1L, dataVersion.getCounter().get()); + Assert.assertEquals(0L, dataVersion.getStateVersion()); + Assert.assertNotEquals(0, rocksdbTopicConfigManager.getTopicConfigTable().size()); + } + + @Test + public void jsonUpgradeToRocksdb() { + if (notToBeExecuted()) { + return; + } + addTopicLoadJsonTopicConfigManager(); + initRocksdbTopicConfigManager(); + DataVersion dataVersion = rocksdbTopicConfigManager.getDataVersion(); + Assert.assertNotNull(dataVersion); + Assert.assertEquals(2L, dataVersion.getCounter().get()); + Assert.assertEquals(0L, dataVersion.getStateVersion()); + Assert.assertNotEquals(0, rocksdbTopicConfigManager.getTopicConfigTable().size()); + Assert.assertEquals(rocksdbTopicConfigManager.getTopicConfigTable().size(), jsonTopicConfigManager.getTopicConfigTable().size()); + + rocksdbTopicConfigManager.stop(); + rocksdbTopicConfigManager = new RocksDBTopicConfigManager(brokerController); + rocksdbTopicConfigManager.load(); + dataVersion = rocksdbTopicConfigManager.getDataVersion(); + Assert.assertEquals(2L, dataVersion.getCounter().get()); + Assert.assertEquals(0L, dataVersion.getStateVersion()); + Assert.assertNotEquals(0, rocksdbTopicConfigManager.getTopicConfigTable().size()); + Assert.assertEquals(rocksdbTopicConfigManager.getTopicConfigTable().size(), rocksdbTopicConfigManager.getTopicConfigTable().size()); + } + + + 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 0000000..3fd1d14 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/topic/TopicConfigManagerTest.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. + */ +package org.apache.rocketmq.broker.topic; + +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +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.Mockito; +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 final String basePath = Paths.get(System.getProperty("user.home"), + "unit-test-store", UUID.randomUUID().toString().substring(0, 16).toUpperCase()).toString(); + 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(); + messageStoreConfig.setStorePathRootDir(basePath); + when(brokerController.getMessageStoreConfig()).thenReturn(messageStoreConfig); + Mockito.lenient().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/TopicQueueMappingCleanServiceTest.java b/broker/src/test/java/org/apache/rocketmq/broker/topic/TopicQueueMappingCleanServiceTest.java new file mode 100644 index 0000000..c7079c5 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/topic/TopicQueueMappingCleanServiceTest.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.broker.topic; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.out.BrokerOuterAPI; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.remoting.protocol.admin.TopicOffset; +import org.apache.rocketmq.remoting.protocol.admin.TopicStatsTable; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.apache.rocketmq.remoting.protocol.statictopic.LogicQueueMappingItem; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingDetail; +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; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +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.mock; +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 TopicQueueMappingCleanServiceTest { + + @Mock + private BrokerController brokerController; + + @Mock + private TopicQueueMappingManager topicQueueMappingManager; + + @Mock + private RpcClient rpcClient; + + @Mock + private MessageStoreConfig messageStoreConfig; + + @Mock + private BrokerConfig brokerConfig; + + @Mock + private BrokerOuterAPI brokerOuterAPI; + + private TopicQueueMappingCleanService topicQueueMappingCleanService; + + private final String defaultTopic = "defaultTopic"; + + private final String defaultBroker = "defaultBroker"; + + private final String deleteWhen = "00;01;02;03;04;05;06;07;08;09;10;11;12;13;14;15;16;17;18;19;20;21;22;23"; + + @Before + public void init() { + when(brokerOuterAPI.getRpcClient()).thenReturn(rpcClient); + when(brokerController.getBrokerOuterAPI()).thenReturn(brokerOuterAPI); + when(brokerController.getTopicQueueMappingManager()).thenReturn(topicQueueMappingManager); + when(brokerController.getMessageStoreConfig()).thenReturn(messageStoreConfig); + when(brokerController.getBrokerConfig()).thenReturn(brokerConfig); + topicQueueMappingCleanService = new TopicQueueMappingCleanService(brokerController); + } + + @Test + public void testCleanItemExpiredNoChange() throws Exception { + when(messageStoreConfig.getDeleteWhen()).thenReturn("04"); + topicQueueMappingCleanService.cleanItemExpired(); + verify(topicQueueMappingManager, never()).updateTopicQueueMapping(any(), anyBoolean(), anyBoolean(), anyBoolean()); + } + + @Test + public void testCleanItemExpiredWithChange() throws Exception { + when(messageStoreConfig.getDeleteWhen()).thenReturn(deleteWhen); + TopicQueueMappingDetail mappingDetail = new TopicQueueMappingDetail(defaultTopic, 2, defaultBroker, 1); + mappingDetail.getHostedQueues().put(0, + Arrays.asList(new LogicQueueMappingItem(0, 0, defaultBroker, 0, 0, 100, 0, 0), + new LogicQueueMappingItem(0, 1, defaultBroker, 1, 100, 200, 0, 0))); + when(topicQueueMappingManager.getTopicQueueMappingTable()).thenReturn(new ConcurrentHashMap<>(Collections.singletonMap(defaultTopic, mappingDetail))); + when(brokerConfig.getBrokerName()).thenReturn(defaultBroker); + TopicStatsTable topicStatsTable = mock(TopicStatsTable.class); + Map offsetTable = new ConcurrentHashMap<>(); + TopicOffset topicOffset = new TopicOffset(); + topicOffset.setMinOffset(0); + topicOffset.setMaxOffset(0); + MessageQueue messageQueue = new MessageQueue(defaultTopic, defaultBroker, 0); + offsetTable.put(messageQueue, topicOffset); + when(topicStatsTable.getOffsetTable()).thenReturn(offsetTable); + when(rpcClient.invoke(any(RpcRequest.class), anyLong())).thenReturn(CompletableFuture.completedFuture(new RpcResponse(0, null, topicStatsTable))); + DataVersion dataVersion = mock(DataVersion.class); + when(topicQueueMappingManager.getDataVersion()).thenReturn(dataVersion); + topicQueueMappingCleanService.cleanItemExpired(); + verify(topicQueueMappingManager, times(1)).updateTopicQueueMapping(any(), anyBoolean(), anyBoolean(), anyBoolean()); + } + + @Test + public void testCleanItemListMoreThanSecondGen() throws Exception { + when(brokerConfig.getBrokerName()).thenReturn(defaultBroker); + when(messageStoreConfig.getDeleteWhen()).thenReturn(deleteWhen); + TopicQueueMappingDetail mappingDetail = new TopicQueueMappingDetail(defaultTopic, 1, defaultBroker, 1); + mappingDetail.setHostedQueues(new ConcurrentHashMap<>()); + LogicQueueMappingItem logicQueueMappingItem = mock(LogicQueueMappingItem.class); + when(logicQueueMappingItem.getBname()).thenReturn("broker"); + mappingDetail.getHostedQueues().put(0, Collections.singletonList(logicQueueMappingItem)); + ConcurrentMap topicQueueMappingTable = new ConcurrentHashMap<>(); + topicQueueMappingTable.put(defaultBroker, mappingDetail); + when(topicQueueMappingManager.getTopicQueueMappingTable()).thenReturn(topicQueueMappingTable); + TopicRouteData topicRouteData = new TopicRouteData(); + when(brokerOuterAPI.getTopicRouteInfoFromNameServer(any(), anyLong())).thenReturn(topicRouteData); + topicQueueMappingCleanService.cleanItemListMoreThanSecondGen(); + verify(brokerOuterAPI, times(1)).getTopicRouteInfoFromNameServer(any(), anyLong()); + } + + @Test + public void testCleanItemListMoreThanSecondGenNoChange() throws Exception { + when(messageStoreConfig.getDeleteWhen()).thenReturn("04"); + topicQueueMappingCleanService.cleanItemListMoreThanSecondGen(); + verify(brokerOuterAPI, never()).getTopicRouteInfoFromNameServer(anyString(), anyLong()); + verify(rpcClient, never()).invoke(any(RpcRequest.class), anyLong()); + } + + @Test + public void testCleanItemListMoreThanSecondGenException() throws Exception { + when(brokerConfig.getBrokerName()).thenReturn(defaultBroker); + when(messageStoreConfig.getDeleteWhen()).thenReturn(deleteWhen); + TopicQueueMappingDetail mappingDetail = new TopicQueueMappingDetail(defaultTopic, 1, defaultBroker, 1); + mappingDetail.setHostedQueues(new ConcurrentHashMap<>()); + LogicQueueMappingItem logicQueueMappingItem = mock(LogicQueueMappingItem.class); + when(logicQueueMappingItem.getBname()).thenReturn("broker"); + mappingDetail.getHostedQueues().put(0, Collections.singletonList(logicQueueMappingItem)); + ConcurrentMap topicQueueMappingTable = new ConcurrentHashMap<>(); + topicQueueMappingTable.put(defaultBroker, mappingDetail); + when(topicQueueMappingManager.getTopicQueueMappingTable()).thenReturn(topicQueueMappingTable); + when(brokerOuterAPI.getTopicRouteInfoFromNameServer(any(), anyLong())).thenThrow(new RemotingException("Test exception")); + topicQueueMappingCleanService.cleanItemListMoreThanSecondGen(); + verify(brokerOuterAPI, times(1)).getTopicRouteInfoFromNameServer(any(), anyLong()); + } +} 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 0000000..b74e57a --- /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 0000000..986b15a --- /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 0000000..690b4ea --- /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 0000000..e01182f --- /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 0000000..b92c07d --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageServiceImplTest.java @@ -0,0 +1,266 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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(), null); + + @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.setTopic("topic"); + 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 0000000..722a306 --- /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 0000000..738690c --- /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 0000000..3a2098e --- /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 0000000..40b12ab --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/util/ServiceProviderTest.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.util; + +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; + +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(); + } +} 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 0000000..2de4c30 --- /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.broker.transaction.AbstractTransactionalMessageCheckListener b/broker/src/test/resources/META-INF/service/org.apache.rocketmq.broker.transaction.AbstractTransactionalMessageCheckListener new file mode 100644 index 0000000..455b266 --- /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 0000000..b012e14 --- /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/rmq.logback-test.xml b/broker/src/test/resources/rmq.logback-test.xml new file mode 100644 index 0000000..7a2ff0b --- /dev/null +++ b/broker/src/test/resources/rmq.logback-test.xml @@ -0,0 +1,37 @@ + + + + + + + ${CONSOLE_LOG_PATTERN} + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/BUILD.bazel b/client/BUILD.bazel new file mode 100644 index 0000000..e6edebe --- /dev/null +++ b/client/BUILD.bazel @@ -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. +# +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//:com_alibaba_fastjson2_fastjson2", + "@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", + "@maven//:commons_codec_commons_codec", + "@maven//:org_yaml_snakeyaml", + ], +) + +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", + "@maven//:org_mockito_mockito_junit_jupiter", + "@maven//:com_alibaba_fastjson2_fastjson2", + ], + resources = glob(["src/test/resources/certs/*.pem"]) + glob(["src/test/resources/certs/*.key"]) + glob(["src/test/resources/**/*.yml"]) +) + +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 new file mode 100644 index 0000000..4f79349 --- /dev/null +++ b/client/pom.xml @@ -0,0 +1,70 @@ + + + + + org.apache.rocketmq + rocketmq-all + 5.3.4-SNAPSHOT + + + 4.0.0 + jar + rocketmq-client + rocketmq-client ${project.version} + + + ${basedir}/.. + + + + + ${project.groupId} + rocketmq-remoting + + + io.netty + netty-tcnative + + + + + org.apache.commons + commons-lang3 + + + io.opentracing + opentracing-api + + + io.opentracing + opentracing-mock + + + io.github.aliyunmq + rocketmq-slf4j-api + + + io.github.aliyunmq + rocketmq-logback-classic + + + org.yaml + snakeyaml + + + diff --git a/client/src/main/java/org/apache/rocketmq/acl/common/AclClientRPCHook.java b/client/src/main/java/org/apache/rocketmq/acl/common/AclClientRPCHook.java new file mode 100644 index 0000000..9fbcc9f --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/acl/common/AclClientRPCHook.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.acl.common; + +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +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(SessionCredentials.ACCESS_KEY, sessionCredentials.getAccessKey()); + // The SecurityToken value is unnecessary,user can choose this one. + if (sessionCredentials.getSecurityToken() != null) { + request.addExtField(SessionCredentials.SECURITY_TOKEN, sessionCredentials.getSecurityToken()); + } + byte[] total = AclUtils.combineRequestContent(request, parseRequestContent(request)); + String signature = AclUtils.calSignature(total, sessionCredentials.getSecretKey()); + request.addExtField(SessionCredentials.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/client/src/main/java/org/apache/rocketmq/acl/common/AclConstants.java b/client/src/main/java/org/apache/rocketmq/acl/common/AclConstants.java new file mode 100644 index 0000000..228e0e2 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/acl/common/AclConstants.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.acl.common; + +public class AclConstants { + + public static final String CONFIG_ACCESS_KEY = "accessKey"; + + public static final String CONFIG_SECRET_KEY = "secretKey"; + + 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"; +} diff --git a/client/src/main/java/org/apache/rocketmq/acl/common/AclException.java b/client/src/main/java/org/apache/rocketmq/acl/common/AclException.java new file mode 100644 index 0000000..54579d4 --- /dev/null +++ b/client/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/client/src/main/java/org/apache/rocketmq/acl/common/AclSigner.java b/client/src/main/java/org/apache/rocketmq/acl/common/AclSigner.java new file mode 100644 index 0000000..a113ec1 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/acl/common/AclSigner.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.acl.common; + +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; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +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/client/src/main/java/org/apache/rocketmq/acl/common/AclUtils.java b/client/src/main/java/org/apache/rocketmq/acl/common/AclUtils.java new file mode 100644 index 0000000..4fbcd0c --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/acl/common/AclUtils.java @@ -0,0 +1,289 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.fastjson2.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 java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.Map; +import java.util.SortedMap; + +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) { + return AclSigner.calSignature(data, secretKey); + } + + 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 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/client/src/main/java/org/apache/rocketmq/acl/common/Permission.java b/client/src/main/java/org/apache/rocketmq/acl/common/Permission.java new file mode 100644 index 0000000..3d7ac81 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/acl/common/Permission.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.acl.common; + +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 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; + } + } +} diff --git a/client/src/main/java/org/apache/rocketmq/acl/common/SessionCredentials.java b/client/src/main/java/org/apache/rocketmq/acl/common/SessionCredentials.java new file mode 100644 index 0000000..95af594 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/acl/common/SessionCredentials.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.acl.common; + +import org.apache.rocketmq.common.MixAll; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Properties; + +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/client/src/main/java/org/apache/rocketmq/acl/common/SigningAlgorithm.java b/client/src/main/java/org/apache/rocketmq/acl/common/SigningAlgorithm.java new file mode 100644 index 0000000..bfed7b2 --- /dev/null +++ b/client/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/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 0000000..d6feb57 --- /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 new file mode 100644 index 0000000..696b073 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/ClientConfig.java @@ -0,0 +1,553 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.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"; + 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 + */ + private int pollNameServerInterval = 1000 * 30; + /** + * Heartbeat interval in microseconds with message broker + */ + private int heartbeatBrokerInterval = 1000 * 30; + /** + * Offset persistent interval for consumer + */ + private int persistConsumerOffsetInterval = 1000 * 5; + private long pullTimeDelayMillsWhenException = 1000; + + private int traceMsgBatchNum = 10; + private boolean unitMode = false; + private String unitName; + 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; + + /** + * The switch for message trace + */ + protected boolean enableTrace = false; + + /** + * The name value of message trace topic. If not set, the default trace topic name will be used. + */ + protected String traceTopic; + + public String buildMQClientId() { + StringBuilder sb = new StringBuilder(); + sb.append(this.getClientIP()); + + sb.append("@"); + sb.append(this.getInstanceName()); + if (!UtilAll.isBlank(this.unitName)) { + sb.append("@"); + sb.append(this.unitName); + } + + if (enableStreamRequestType) { + sb.append("@"); + sb.append(RequestType.STREAM); + } + + return sb.toString(); + } + + public int getTraceMsgBatchNum() { + return traceMsgBatchNum; + } + + public void setTraceMsgBatchNum(int traceMsgBatchNum) { + this.traceMsgBatchNum = traceMsgBatchNum; + } + + public String getClientIP() { + return clientIP; + } + + public void setClientIP(String clientIP) { + this.clientIP = clientIP; + } + + public String getInstanceName() { + return instanceName; + } + + public void setInstanceName(String instanceName) { + this.instanceName = instanceName; + } + + public void changeInstanceNameToPID() { + if (this.instanceName.equals("DEFAULT")) { + 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) { + this.namesrvAddr = cc.namesrvAddr; + this.clientIP = cc.clientIP; + this.instanceName = cc.instanceName; + this.clientCallbackExecutorThreads = cc.clientCallbackExecutorThreads; + 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; + this.enableTrace = cc.enableTrace; + this.traceTopic = cc.traceTopic; + } + + public ClientConfig cloneClientConfig() { + ClientConfig cc = new ClientConfig(); + cc.namesrvAddr = namesrvAddr; + cc.clientIP = clientIP; + cc.instanceName = instanceName; + cc.clientCallbackExecutorThreads = clientCallbackExecutorThreads; + 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; + cc.enableTrace = enableTrace; + cc.traceTopic = traceTopic; + 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() { + return clientCallbackExecutorThreads; + } + + public void setClientCallbackExecutorThreads(int clientCallbackExecutorThreads) { + this.clientCallbackExecutorThreads = clientCallbackExecutorThreads; + } + + public int getPollNameServerInterval() { + return pollNameServerInterval; + } + + public void setPollNameServerInterval(int pollNameServerInterval) { + this.pollNameServerInterval = pollNameServerInterval; + } + + public int getHeartbeatBrokerInterval() { + return heartbeatBrokerInterval; + } + + public void setHeartbeatBrokerInterval(int heartbeatBrokerInterval) { + this.heartbeatBrokerInterval = heartbeatBrokerInterval; + } + + public int getPersistConsumerOffsetInterval() { + return persistConsumerOffsetInterval; + } + + 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; + } + + public void setUnitName(String unitName) { + this.unitName = unitName; + } + + public boolean isUnitMode() { + return unitMode; + } + + public void setUnitMode(boolean unitMode) { + this.unitMode = unitMode; + } + + public boolean isVipChannelEnabled() { + return vipChannelEnabled; + } + + public void setVipChannelEnabled(final boolean vipChannelEnabled) { + this.vipChannelEnabled = vipChannelEnabled; + } + + public boolean isUseTLS() { + return useTLS; + } + + 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; + } + + public boolean isEnableTrace() { + return enableTrace; + } + + public void setEnableTrace(boolean enableTrace) { + this.enableTrace = enableTrace; + } + + public String getTraceTopic() { + return traceTopic; + } + + public void setTraceTopic(String traceTopic) { + this.traceTopic = traceTopic; + } + + @Override + public String toString() { + 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 + + ", enableTrace=" + enableTrace + + ", traceTopic='" + traceTopic + '\'' + + '}'; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/MQAdmin.java b/client/src/main/java/org/apache/rocketmq/client/MQAdmin.java new file mode 100644 index 0000000..4864add --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/MQAdmin.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.client; + +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.message.MessageExt; +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 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, Map attributes) + throws MQClientException; + + /** + * 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, Map attributes) + throws MQClientException; + + /** + * Gets the message queue offset according to some time in milliseconds
+ * be cautious to call because of more IO overhead + * + * @param mq Instance of MessageQueue + * @param timestamp from when in milliseconds. + * @return offset + */ + long searchOffset(final MessageQueue mq, final long timestamp) throws MQClientException; + + /** + * Gets the max offset + * + * @param mq Instance of MessageQueue + * @return the max offset + */ + long maxOffset(final MessageQueue mq) throws MQClientException; + + /** + * Gets the minimum offset + * + * @param mq Instance of MessageQueue + * @return the minimum offset + */ + long minOffset(final MessageQueue mq) throws MQClientException; + + /** + * Gets the earliest stored message time + * + * @param mq Instance of MessageQueue + * @return the time in microseconds + */ + long earliestMsgStoreTime(final MessageQueue mq) throws MQClientException; + + /** + * Query messages + * + * @param topic message topic + * @param key message key index word + * @param maxNum max message number + * @param begin from when + * @param end to when + * @return Instance of QueryResult + */ + QueryResult queryMessage(final String topic, final String key, final int maxNum, final long begin, + final long end) throws MQClientException, InterruptedException; + + /** + * @return The {@code MessageExt} of given msgId + */ + MessageExt viewMessage(String topic, + String msgId) throws RemotingException, MQBrokerException, InterruptedException, MQClientException; + +} \ No newline at end of file diff --git a/client/src/main/java/org/apache/rocketmq/client/MQHelper.java b/client/src/main/java/org/apache/rocketmq/client/MQHelper.java new file mode 100644 index 0000000..9da6f5b --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/MQHelper.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; + +import java.util.Set; +import java.util.TreeSet; +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.common.message.MessageQueue; +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, + final String topic, + final long timestamp) throws Exception { + resetOffsetByTimestamp(messageModel, "DEFAULT", consumerGroup, topic, timestamp); + } + + /** + * Reset consumer topic offset according to time + * + * @param messageModel which model + * @param instanceName which instance + * @param consumerGroup consumer group + * @param topic topic + * @param timestamp time + */ + public static void resetOffsetByTimestamp( + final MessageModel messageModel, + final String instanceName, + final String consumerGroup, + final String topic, + final long timestamp) throws Exception { + + DefaultMQPullConsumer consumer = new DefaultMQPullConsumer(consumerGroup); + consumer.setInstanceName(instanceName); + consumer.setMessageModel(messageModel); + consumer.start(); + + Set mqs = null; + try { + mqs = consumer.fetchSubscribeMessageQueues(topic); + if (mqs != null && !mqs.isEmpty()) { + TreeSet mqsNew = new TreeSet<>(mqs); + for (MessageQueue mq : mqsNew) { + long offset = consumer.searchOffset(mq, timestamp); + if (offset >= 0) { + consumer.updateConsumeOffset(mq, offset); + log.info("resetOffsetByTimestamp updateConsumeOffset success, {} {} {}", + consumerGroup, offset, mq); + } + } + } + } catch (Exception e) { + log.warn("resetOffsetByTimestamp Exception", e); + throw e; + } finally { + if (mqs != null) { + consumer.getDefaultMQPullConsumerImpl().getOffsetStore().persistAll(mqs); + } + consumer.shutdown(); + } + } +} 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 0000000..4eb74c0 --- /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/QueryResult.java b/client/src/main/java/org/apache/rocketmq/client/QueryResult.java new file mode 100644 index 0000000..a5e2050 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/QueryResult.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; + +import java.util.List; +import org.apache.rocketmq.common.message.MessageExt; + +public class QueryResult { + private final long indexLastUpdateTimestamp; + private final List messageList; + + public QueryResult(long indexLastUpdateTimestamp, List messageList) { + this.indexLastUpdateTimestamp = indexLastUpdateTimestamp; + this.messageList = messageList; + } + + public long getIndexLastUpdateTimestamp() { + return indexLastUpdateTimestamp; + } + + public List getMessageList() { + return messageList; + } + + @Override + public String toString() { + return "QueryResult [indexLastUpdateTimestamp=" + indexLastUpdateTimestamp + ", messageList=" + + messageList + "]"; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/Validators.java b/client/src/main/java/org/apache/rocketmq/client/Validators.java new file mode 100644 index 0000000..77e4bbd --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/Validators.java @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.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.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 int CHARACTER_MAX_LENGTH = 255; + public static final int TOPIC_MAX_LENGTH = 127; + + /** + * Validate group + */ + public static void checkGroup(String group) throws MQClientException { + if (UtilAll.isBlank(group)) { + throw new MQClientException("the specified group is blank", null); + } + + if (group.length() > CHARACTER_MAX_LENGTH) { + throw new MQClientException("the specified group is longer than group max length 255.", null); + } + + + if (isTopicOrGroupIllegal(group)) { + throw new MQClientException(String.format( + "the specified group[%s] contains illegal characters, allowing only %s", group, + "^[%|a-zA-Z0-9_-]+$"), null); + } + } + + 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()) { + throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message body is null"); + } + + if (0 == msg.getBody().length) { + throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message body length is zero"); + } + + if (msg.getBody().length > defaultMQProducer.getMaxMessageSize()) { + 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"); + } + } + + public static void checkTopic(String topic) throws MQClientException { + if (UtilAll.isBlank(topic)) { + throw new MQClientException("The specified topic is blank", null); + } + + 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, + "^[%|a-zA-Z0-9_-]+$"), 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); + } + } + + public static void isNotAllowedSendTopic(String topic) throws MQClientException { + if (TopicValidator.isNotAllowedSendTopic(topic)) { + throw new MQClientException( + 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/admin/MQAdminExtInner.java b/client/src/main/java/org/apache/rocketmq/client/admin/MQAdminExtInner.java new file mode 100644 index 0000000..a6e86c8 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/admin/MQAdminExtInner.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.client.admin; + +public interface MQAdminExtInner { + +} 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 new file mode 100644 index 0000000..bc03b14 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/common/ClientErrorCode.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.common; + +public class ClientErrorCode { + public static final int CONNECT_BROKER_EXCEPTION = 10001; + public static final int ACCESS_BROKER_TIMEOUT = 10002; + 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 0000000..2cdae48 --- /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 new file mode 100644 index 0000000..c15cdbf --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/common/ThreadLocalIndex.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.common; + +import java.util.Random; + +public class ThreadLocalIndex { + private final ThreadLocal threadLocalIndex = new ThreadLocal<>(); + private final Random random = new Random(); + private final static int POSITIVE_MASK = 0x7FFFFFFF; + + public int incrementAndGet() { + Integer index = this.threadLocalIndex.get(); + if (null == index) { + index = random.nextInt(); + } + this.threadLocalIndex.set(++index); + return index & POSITIVE_MASK; + } + + public void reset() { + int index = Math.abs(random.nextInt(Integer.MAX_VALUE)); + this.threadLocalIndex.set(index); + } + + @Override + public String toString() { + return "ThreadLocalIndex{" + + "threadLocalIndex=" + threadLocalIndex.get() + + '}'; + } +} 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 0000000..99a261c --- /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 0000000..06cb59a --- /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 0000000..b144f8f --- /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/AllocateMessageQueueStrategy.java b/client/src/main/java/org/apache/rocketmq/client/consumer/AllocateMessageQueueStrategy.java new file mode 100644 index 0000000..c1f0604 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/AllocateMessageQueueStrategy.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.client.consumer; + +import java.util.List; +import org.apache.rocketmq.common.message.MessageQueue; + +/** + * Strategy Algorithm for message allocating between consumers + */ +public interface AllocateMessageQueueStrategy { + + /** + * Allocating by consumer id + * + * @param consumerGroup current consumer group + * @param currentCID current consumer id + * @param mqAll message queue set in current topic + * @param cidAll consumer set in current consumer group + * @return The allocate result of given strategy + */ + List allocate( + final String consumerGroup, + final String currentCID, + final List mqAll, + final List cidAll + ); + + /** + * Algorithm name + * + * @return The strategy name + */ + String getName(); +} 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 0000000..20857f1 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/DefaultLitePullConsumer.java @@ -0,0 +1,621 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; + + private RPCHook rpcHook; + + /** + * 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.rpcHook = rpcHook; + 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.rpcHook = rpcHook; + 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; + } + + private void setTraceDispatcher() { + if (enableTrace) { + try { + AsyncTraceDispatcher traceDispatcher = new AsyncTraceDispatcher(consumerGroup, TraceDispatcher.Type.CONSUME, getTraceMsgBatchNum(), traceTopic, rpcHook); + traceDispatcher.getTraceProducer().setUseTLS(this.isUseTLS()); + traceDispatcher.setNamespaceV2(namespaceV2); + 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 traceTopic; + } + + public void setCustomizedTraceTopic(String customizedTraceTopic) { + this.traceTopic = customizedTraceTopic; + } + + public boolean isEnableMsgTrace() { + return enableTrace; + } + + public void setEnableMsgTrace(boolean enableMsgTrace) { + this.enableTrace = 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 new file mode 100644 index 0000000..38841e4 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/DefaultMQPullConsumer.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.consumer; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.QueryResult; +import org.apache.rocketmq.client.consumer.rebalance.AllocateMessageQueueAveragely; +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.impl.consumer.DefaultMQPullConsumerImpl; +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.MessageQueue; +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.filter.FilterAPI; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; + +/** + * @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 + */ + 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; + /** + * Topic set you want to register + */ + private Set registerTopics = new HashSet<>(); + + private final Set registerSubscriptions = Collections.newSetFromMap(new ConcurrentHashMap<>()); + /** + * Queue allocation algorithm + */ + private AllocateMessageQueueStrategy allocateMessageQueueStrategy = new AllocateMessageQueueAveragely(); + /** + * Whether the unit of subscription group + */ + private boolean unitMode = false; + + private int maxReconsumeTimes = 16; + + private boolean enableRebalance = true; + + public DefaultMQPullConsumer() { + this(MixAll.DEFAULT_CONSUMER_GROUP, null); + } + + public DefaultMQPullConsumer(final String consumerGroup) { + this(consumerGroup, null); + } + + 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, + 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, + 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(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(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(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(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 QueryResult queryMessage(String topic, String key, int maxNum, long begin, long end) + throws MQClientException, InterruptedException { + return this.defaultMQPullConsumerImpl.queryMessage(withNamespace(topic), key, maxNum, begin, end); + } + + public AllocateMessageQueueStrategy getAllocateMessageQueueStrategy() { + return allocateMessageQueueStrategy; + } + + public void setAllocateMessageQueueStrategy(AllocateMessageQueueStrategy allocateMessageQueueStrategy) { + this.allocateMessageQueueStrategy = allocateMessageQueueStrategy; + } + + 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; + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + 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 MessageModel getMessageModel() { + return messageModel; + } + + public void setMessageModel(MessageModel messageModel) { + this.messageModel = messageModel; + } + + public MessageQueueListener getMessageQueueListener() { + return messageQueueListener; + } + + public void setMessageQueueListener(MessageQueueListener messageQueueListener) { + this.messageQueueListener = messageQueueListener; + } + + public Set getRegisterTopics() { + return registerTopics; + } + + public void setRegisterTopics(Set registerTopics) { + this.registerTopics = withNamespace(registerTopics); + } + + public Set getRegisterSubscriptions() { + return registerSubscriptions; + } + + public void addRegisterSubscriptions(String topic, MessageSelector messageSelector) throws MQClientException { + try { + if (messageSelector == null) { + messageSelector = MessageSelector.byTag(SubscriptionData.SUB_ALL); + } + + SubscriptionData subscriptionData = FilterAPI.build(withNamespace(topic), + messageSelector.getExpression(), messageSelector.getExpressionType()); + + this.registerSubscriptions.add(subscriptionData); + } catch (Exception e) { + throw new MQClientException("add subscription exception", e); + } + } + + public void clearRegisterSubscriptions() { + this.registerSubscriptions.clear(); + } + + /** + * 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, msg.getBrokerName()); + } + + /** + * 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(withNamespace(topic)); + } + + @Override + public void start() throws MQClientException { + this.setConsumerGroup(NamespaceUtil.wrapNamespace(this.getNamespace(), this.consumerGroup)); + this.defaultMQPullConsumerImpl.start(); + } + + @Override + public void shutdown() { + this.defaultMQPullConsumerImpl.shutdown(); + } + + @Override + public void registerMessageQueueListener(String topic, MessageQueueListener listener) { + synchronized (this.registerTopics) { + this.registerTopics.add(withNamespace(topic)); + if (listener != null) { + this.messageQueueListener = listener; + } + } + } + + @Override + public PullResult pull(MessageQueue mq, String subExpression, long offset, int maxNums) + throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + 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(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(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(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(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(queueWithNamespace(mq), subExpression, offset, maxNums, pullCallback); + } + + @Override + public void pullBlockIfNotFoundWithMessageSelector(MessageQueue mq, MessageSelector selector, + long offset, int maxNums, + PullCallback pullCallback) throws MQClientException, RemotingException, InterruptedException { + this.defaultMQPullConsumerImpl.pullBlockIfNotFoundWithMessageSelector(mq, selector, offset, maxNums, pullCallback); + } + + @Override + public PullResult pullBlockIfNotFoundWithMessageSelector(MessageQueue mq, MessageSelector selector, + long offset, + int maxNums) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + return this.defaultMQPullConsumerImpl.pullBlockIfNotFoundWithMessageSelector(mq, selector, offset, maxNums); + } + + @Override + public void updateConsumeOffset(MessageQueue mq, long offset) throws MQClientException { + this.defaultMQPullConsumerImpl.updateConsumeOffset(queueWithNamespace(mq), offset); + } + + @Override + public long fetchConsumeOffset(MessageQueue mq, boolean fromStore) throws MQClientException { + return this.defaultMQPullConsumerImpl.fetchConsumeOffset(queueWithNamespace(mq), fromStore); + } + + @Override + public Set fetchMessageQueuesInBalance(String topic) throws MQClientException { + return this.defaultMQPullConsumerImpl.fetchMessageQueuesInBalance(withNamespace(topic)); + } + + @Override + public MessageExt viewMessage(String topic, + String uniqKey) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + try { + MessageDecoder.decodeMessageId(uniqKey); + return this.defaultMQPullConsumerImpl.viewMessage(topic, uniqKey); + } catch (Exception e) { + // Ignore + } + 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; + } + + public int getMaxReconsumeTimes() { + return maxReconsumeTimes; + } + + public void setMaxReconsumeTimes(final int maxReconsumeTimes) { + this.maxReconsumeTimes = maxReconsumeTimes; + } + + public void persist(MessageQueue mq) { + this.getOffsetStore().persist(queueWithNamespace(mq)); + } + + public boolean isEnableRebalance() { + return enableRebalance; + } + + public void setEnableRebalance(boolean enableRebalance) { + this.enableRebalance = enableRebalance; + } +} 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 new file mode 100644 index 0000000..5df5cc8 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/DefaultMQPushConsumer.java @@ -0,0 +1,1005 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.ClientConfig; +import org.apache.rocketmq.client.QueryResult; +import org.apache.rocketmq.client.consumer.listener.MessageListener; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; +import org.apache.rocketmq.client.consumer.rebalance.AllocateMessageQueueAveragely; +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.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +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 java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** + * In most scenarios, this is the mostly recommended class to consume messages. + *

+ * Technically speaking, this push client is virtually a wrapper of the underlying pull service. Specifically, on + * arrival of messages pulled from brokers, it roughly invokes the registered callback handler to feed the messages. + *

+ * See quickstart/Consumer in the example module for a typical usage. + *

+ * + *

+ * Thread Safety: After initialization, the instance can be regarded as thread-safe. + *

+ */ +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. + */ + protected final transient DefaultMQPushConsumerImpl defaultMQPushConsumerImpl; + + /** + * Consumers of the same role is required to have exactly same subscriptions and consumerGroup to correctly achieve + * load balance. It's required and needs to be globally unique. + *

+ * See here for further discussion. + */ + private String consumerGroup; + + /** + * Message model defines the way how messages are delivered to each consumer clients. + *

+ * RocketMQ supports two message models: clustering and broadcasting. If clustering is set, consumer clients with + * the same {@link #consumerGroup} would only consume shards of the messages subscribed, which achieves load + * balances; Conversely, if the broadcasting is set, each consumer client will consume all subscribed messages + * separately. + *

+ * This field defaults to clustering. + */ + private MessageModel messageModel = MessageModel.CLUSTERING; + + /** + * Consuming point on consumer booting. + *

+ * There are three consuming points: + *
    + *
  • + * CONSUME_FROM_LAST_OFFSET: consumer clients pick up where it stopped previously. + * If it were a newly booting up consumer client, according aging of the consumer group, there are two + * cases: + *
      + *
    1. + * if the consumer group is created so recently that the earliest message being subscribed has yet + * expired, which means the consumer group represents a lately launched business, consuming will + * start from the very beginning; + *
    2. + *
    3. + * if the earliest message being subscribed has expired, consuming will start from the latest + * messages, meaning messages born prior to the booting timestamp would be ignored. + *
    4. + *
    + *
  • + *
  • + * CONSUME_FROM_FIRST_OFFSET: Consumer client will start from earliest messages available. + *
  • + *
  • + * CONSUME_FROM_TIMESTAMP: Consumer client will start from specified timestamp, which means + * messages born prior to {@link #consumeTimestamp} will be ignored + *
  • + *
+ */ + 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)); + + /** + * Queue allocation algorithm specifying how message queues are allocated to each consumer clients. + */ + private AllocateMessageQueueStrategy allocateMessageQueueStrategy; + + /** + * Subscription relationship + */ + private Map subscription = new HashMap<>(); + + /** + * Message listener + */ + private MessageListener messageListener; + + /** + * Listener to call if message queue assignment is changed. + */ + private MessageQueueListener messageQueueListener; + + /** + * Offset Storage + */ + private OffsetStore offsetStore; + + /** + * Minimum consumer thread number + */ + private int consumeThreadMin = 20; + + /** + * Max consumer thread number + */ + private int consumeThreadMax = 20; + + /** + * Threshold for dynamic adjustment of the number of thread pool + */ + private long adjustThreadPoolNumsThreshold = 100000; + + /** + * Concurrently max span offset.it has no effect on sequential consumption + */ + private int consumeConcurrentlyMaxSpan = 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; + + /** + * 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(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 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 + */ + private int pullThresholdForTopic = -1; + + /** + * Limit the cached message size on topic level, default value is -1 MiB(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 + */ + private int pullThresholdSizeForTopic = -1; + + /** + * Message pull Interval + */ + private long pullInterval = 0; + + /** + * Batch consumption size + */ + private int consumeMessageBatchMaxSize = 1; + + /** + * Batch pull size + */ + private int pullBatchSize = 32; + + private int pullBatchSizeInBytes = 256 * 1024; + + /** + * Whether update subscription relationship when every pull + */ + private boolean postSubscriptionWhenPull = false; + + /** + * Whether the unit of subscription group + */ + private boolean unitMode = false; + + /** + * 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. + */ + private int maxReconsumeTimes = -1; + + /** + * Suspending pulling time for cases requiring slow pulling like flow-control scenario. + */ + private long suspendCurrentQueueTimeMillis = 1000; + + /** + * Maximum amount of time in minutes a message may block the consuming thread. + */ + 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; + + private RPCHook rpcHook = null; + + /** + * Default constructor. + */ + 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, rpcHook, new AllocateMessageQueueAveragely()); + } + + /** + * 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 Consumer group. + * @param rpcHook RPC hook to execute before each remoting command. + * @param allocateMessageQueueStrategy Message queue allocating algorithm. + */ + public DefaultMQPushConsumer(final String consumerGroup, RPCHook rpcHook, + AllocateMessageQueueStrategy allocateMessageQueueStrategy) { + this(consumerGroup, rpcHook, allocateMessageQueueStrategy, false, null); + } + + /** + * Constructor specifying consumer group, RPC hook, message queue allocating algorithm, enabled msg trace flag and customized trace topic name. + * + * @param consumerGroup Consumer group. + * @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(final String consumerGroup, RPCHook rpcHook, + AllocateMessageQueueStrategy allocateMessageQueueStrategy, boolean enableMsgTrace, + final String customizedTraceTopic) { + this.consumerGroup = consumerGroup; + this.rpcHook = rpcHook; + this.allocateMessageQueueStrategy = allocateMessageQueueStrategy; + defaultMQPushConsumerImpl = new DefaultMQPushConsumerImpl(this, rpcHook); + this.enableTrace = enableMsgTrace; + this.traceTopic = customizedTraceTopic; + } + + /** + * Constructor specifying namespace and consumer group. + * + * @param namespace Namespace for this MQ Producer instance. + * @param consumerGroup Consumer group. + */ + @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 Consumer group. + * @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.rpcHook = rpcHook; + 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 Consumer group. + * @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.rpcHook = rpcHook; + this.allocateMessageQueueStrategy = allocateMessageQueueStrategy; + defaultMQPushConsumerImpl = new DefaultMQPushConsumerImpl(this, rpcHook); + this.enableTrace = enableMsgTrace; + this.traceTopic = customizedTraceTopic; + } + + /** + * 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, + Map attributes) throws MQClientException { + createTopic(key, withNamespace(newTopic), queueNum, 0, null); + } + + @Override + public void setUseTLS(boolean useTLS) { + super.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(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(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(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(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 QueryResult queryMessage(String topic, String key, int maxNum, long begin, long end) + throws MQClientException, InterruptedException { + 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 { + try { + MessageDecoder.decodeMessageId(msgId); + return this.defaultMQPushConsumerImpl.viewMessage(withNamespace(topic), msgId); + } catch (Exception e) { + // Ignore + } + return this.defaultMQPushConsumerImpl.queryMessageByUniqKey(withNamespace(topic), msgId); + } + + public AllocateMessageQueueStrategy getAllocateMessageQueueStrategy() { + return allocateMessageQueueStrategy; + } + + public void setAllocateMessageQueueStrategy(AllocateMessageQueueStrategy allocateMessageQueueStrategy) { + this.allocateMessageQueueStrategy = allocateMessageQueueStrategy; + } + + public int getConsumeConcurrentlyMaxSpan() { + return consumeConcurrentlyMaxSpan; + } + + public void setConsumeConcurrentlyMaxSpan(int consumeConcurrentlyMaxSpan) { + this.consumeConcurrentlyMaxSpan = consumeConcurrentlyMaxSpan; + } + + public ConsumeFromWhere getConsumeFromWhere() { + return consumeFromWhere; + } + + public void setConsumeFromWhere(ConsumeFromWhere consumeFromWhere) { + this.consumeFromWhere = consumeFromWhere; + } + + public int getConsumeMessageBatchMaxSize() { + return consumeMessageBatchMaxSize; + } + + public void setConsumeMessageBatchMaxSize(int consumeMessageBatchMaxSize) { + this.consumeMessageBatchMaxSize = consumeMessageBatchMaxSize; + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public int getConsumeThreadMax() { + return consumeThreadMax; + } + + public void setConsumeThreadMax(int consumeThreadMax) { + this.consumeThreadMax = consumeThreadMax; + } + + public int getConsumeThreadMin() { + return consumeThreadMin; + } + + 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; + } + + public MessageListener getMessageListener() { + return messageListener; + } + + public void setMessageListener(MessageListener messageListener) { + this.messageListener = messageListener; + } + + public MessageModel getMessageModel() { + return messageModel; + } + + public void setMessageModel(MessageModel messageModel) { + this.messageModel = messageModel; + } + + public int getPullBatchSize() { + return pullBatchSize; + } + + public void setPullBatchSize(int pullBatchSize) { + this.pullBatchSize = pullBatchSize; + } + + public long getPullInterval() { + return pullInterval; + } + + public void setPullInterval(long pullInterval) { + this.pullInterval = pullInterval; + } + + public int getPullThresholdForQueue() { + return pullThresholdForQueue; + } + + 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; + } + + public void setPullThresholdForTopic(final int pullThresholdForTopic) { + this.pullThresholdForTopic = pullThresholdForTopic; + } + + public int getPullThresholdSizeForQueue() { + return pullThresholdSizeForQueue; + } + + public void setPullThresholdSizeForQueue(final int pullThresholdSizeForQueue) { + this.pullThresholdSizeForQueue = pullThresholdSizeForQueue; + } + + public int getPullThresholdSizeForTopic() { + return pullThresholdSizeForTopic; + } + + public void setPullThresholdSizeForTopic(final int pullThresholdSizeForTopic) { + this.pullThresholdSizeForTopic = pullThresholdSizeForTopic; + } + + 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) { + 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. + * @throws MQBrokerException if there is any broker error. + * @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 { + msg.setTopic(withNamespace(msg.getTopic())); + this.defaultMQPushConsumerImpl.sendMessageBack(msg, delayLevel, msg.getBrokerName()); + } + + /** + * 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. + * @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 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(withNamespace(topic)); + } + + /** + * This method gets internal infrastructure readily to serve. Instances must call this method after configuration. + * + * @throws MQClientException if there is any client error. + */ + @Override + public void start() throws MQClientException { + setConsumerGroup(NamespaceUtil.wrapNamespace(this.getNamespace(), this.consumerGroup)); + this.defaultMQPushConsumerImpl.start(); + if (enableTrace) { + try { + AsyncTraceDispatcher dispatcher = new AsyncTraceDispatcher(consumerGroup, TraceDispatcher.Type.CONSUME, getTraceMsgBatchNum(), traceTopic, rpcHook); + dispatcher.setHostConsumer(this.defaultMQPushConsumerImpl); + dispatcher.setNamespaceV2(namespaceV2); + 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"); + } + } + if (null != traceDispatcher) { + if (traceDispatcher instanceof AsyncTraceDispatcher) { + ((AsyncTraceDispatcher) traceDispatcher).getTraceProducer().setUseTLS(isUseTLS()); + } + try { + traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel()); + } catch (MQClientException e) { + log.warn("trace dispatcher start failed ", e); + } + } + } + + /** + * Shut down this client and releasing underlying resources. + */ + @Override + public void shutdown() { + this.defaultMQPushConsumerImpl.shutdown(awaitTerminationMillisWhenShutdown); + if (null != traceDispatcher) { + traceDispatcher.shutdown(); + } + } + + @Override + @Deprecated + public void registerMessageListener(MessageListener messageListener) { + this.messageListener = messageListener; + this.defaultMQPushConsumerImpl.registerMessageListener(messageListener); + } + + /** + * Register a callback to execute on message arrival for concurrent consuming. + * + * @param messageListener message handling callback. + */ + @Override + public void registerMessageListener(MessageListenerConcurrently messageListener) { + this.messageListener = messageListener; + this.defaultMQPushConsumerImpl.registerMessageListener(messageListener); + } + + /** + * Register a callback to execute on message arrival for orderly consuming. + * + * @param messageListener message handling callback. + */ + @Override + public void registerMessageListener(MessageListenerOrderly messageListener) { + this.messageListener = messageListener; + this.defaultMQPushConsumerImpl.registerMessageListener(messageListener); + } + + /** + * Subscribe a topic to consuming subscription. + * + * @param topic topic to subscribe. + * @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. + */ + @Override + public void subscribe(String topic, String subExpression) throws MQClientException { + this.defaultMQPushConsumerImpl.subscribe(withNamespace(topic), subExpression); + } + + /** + * Subscribe a topic to consuming subscription. + * + * @param topic topic to consume. + * @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 + */ + @Override + public void subscribe(String topic, String fullClassName, String filterClassSource) throws MQClientException { + this.defaultMQPushConsumerImpl.subscribe(withNamespace(topic), fullClassName, filterClassSource); + } + + /** + * Subscribe a topic by message selector. + * + * @param topic topic to consume. + * @param messageSelector {@link org.apache.rocketmq.client.consumer.MessageSelector} + * @see org.apache.rocketmq.client.consumer.MessageSelector#bySql + * @see org.apache.rocketmq.client.consumer.MessageSelector#byTag + */ + @Override + public void subscribe(final String topic, final MessageSelector messageSelector) throws MQClientException { + this.defaultMQPushConsumerImpl.subscribe(withNamespace(topic), messageSelector); + } + + /** + * Un-subscribe the specified topic from subscription. + * + * @param topic message topic + */ + @Override + public void unsubscribe(String topic) { + this.defaultMQPushConsumerImpl.unsubscribe(topic); + } + + /** + * Update the message consuming thread core pool size. + * + * @param corePoolSize new core pool size. + */ + @Override + public void updateCorePoolSize(int corePoolSize) { + this.defaultMQPushConsumerImpl.updateCorePoolSize(corePoolSize); + } + + /** + * Suspend pulling new messages. + */ + @Override + public void suspend() { + this.defaultMQPushConsumerImpl.suspend(); + } + + /** + * Resume pulling. + */ + @Override + 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; + } + + public String getConsumeTimestamp() { + return consumeTimestamp; + } + + public void setConsumeTimestamp(String consumeTimestamp) { + this.consumeTimestamp = consumeTimestamp; + } + + public boolean isPostSubscriptionWhenPull() { + return postSubscriptionWhenPull; + } + + public void setPostSubscriptionWhenPull(boolean postSubscriptionWhenPull) { + this.postSubscriptionWhenPull = postSubscriptionWhenPull; + } + + @Override + public boolean isUnitMode() { + return unitMode; + } + + @Override + public void setUnitMode(boolean isUnitMode) { + this.unitMode = isUnitMode; + } + + public long getAdjustThreadPoolNumsThreshold() { + return adjustThreadPoolNumsThreshold; + } + + public void setAdjustThreadPoolNumsThreshold(long adjustThreadPoolNumsThreshold) { + this.adjustThreadPoolNumsThreshold = adjustThreadPoolNumsThreshold; + } + + public int getMaxReconsumeTimes() { + return maxReconsumeTimes; + } + + public void setMaxReconsumeTimes(final int maxReconsumeTimes) { + this.maxReconsumeTimes = maxReconsumeTimes; + } + + public long getSuspendCurrentQueueTimeMillis() { + return suspendCurrentQueueTimeMillis; + } + + public void setSuspendCurrentQueueTimeMillis(final long suspendCurrentQueueTimeMillis) { + this.suspendCurrentQueueTimeMillis = suspendCurrentQueueTimeMillis; + } + + public long getConsumeTimeout() { + return consumeTimeout; + } + + 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 0000000..6c6a597 --- /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 new file mode 100644 index 0000000..81e06ee --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/MQConsumer.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; + +import java.util.Set; +import org.apache.rocketmq.client.MQAdmin; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.exception.RemotingException; + +/** + * Message queue consumer interface + */ +public interface MQConsumer extends MQAdmin { + /** + * 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 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 pertaining to the given topic. + * + * @param topic message topic + * @return queue set + */ + Set fetchSubscribeMessageQueues(final String topic) throws MQClientException; +} 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 new file mode 100644 index 0000000..ee77b12 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/MQPullConsumer.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.client.consumer; + +import java.util.Set; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.exception.RemotingException; + +/** + * Pulling consumer interface + */ +public interface MQPullConsumer extends MQConsumer { + /** + * Start the consumer + */ + void start() throws MQClientException; + + /** + * Shutdown the consumer + */ + void shutdown(); + + /** + * Register the message queue listener + */ + void registerMessageQueueListener(final String topic, final MessageQueueListener listener); + + /** + * Pulling the messages,not blocking + * + * @param mq from which message queue + * @param subExpression subscription expression.it only support or operation such as "tag1 || tag2 || tag3"
if + * null or * expression,meaning subscribe all + * @param offset from where to pull + * @param maxNums max pulling numbers + * @return The resulting {@code PullRequest} + */ + PullResult pull(final MessageQueue mq, final String subExpression, final long offset, + final int maxNums) throws MQClientException, RemotingException, MQBrokerException, + InterruptedException; + + /** + * Pulling the messages in the specified timeout + * + * @return The resulting {@code PullRequest} + */ + PullResult pull(final MessageQueue mq, final String subExpression, final long offset, + 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 + */ + void pull(final MessageQueue mq, final String subExpression, final long offset, final int maxNums, + final PullCallback pullCallback) 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 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 + * + * @return The resulting {@code PullRequest} + */ + PullResult pullBlockIfNotFound(final MessageQueue mq, final String subExpression, + final long offset, final int maxNums) throws MQClientException, RemotingException, + MQBrokerException, InterruptedException; + + /** + * Pulling the messages through callback function,if no message arrival,blocking. + */ + void pullBlockIfNotFound(final MessageQueue mq, final String subExpression, final long offset, + final int maxNums, final PullCallback pullCallback) throws MQClientException, RemotingException, + InterruptedException; + + /** + * Pulling the messages through callback function,if no message arrival,blocking. Support message selection + */ + void pullBlockIfNotFoundWithMessageSelector(final MessageQueue mq, final MessageSelector selector, + final long offset, final int maxNums, + final PullCallback pullCallback) throws MQClientException, RemotingException, + InterruptedException; + + /** + * Pulling the messages,if no message arrival,blocking some time. Support message selection + * + * @return The resulting {@code PullRequest} + */ + PullResult pullBlockIfNotFoundWithMessageSelector(final MessageQueue mq, final MessageSelector selector, + final long offset, final int maxNums) throws MQClientException, RemotingException, + MQBrokerException, InterruptedException; + + /** + * Update the offset + */ + void updateConsumeOffset(final MessageQueue mq, final long offset) throws MQClientException; + + /** + * Fetch the offset + * + * @return The fetched offset of given queue + */ + long fetchConsumeOffset(final MessageQueue mq, final boolean fromStore) throws MQClientException; + + /** + * Fetch the message queues according to the topic + * + * @param topic message topic + * @return message queue set + */ + Set fetchMessageQueuesInBalance(final String topic) throws MQClientException; + + /** + * If consuming failure,message will be send back to the broker,and delay consuming in some time later.
+ * Mind! message can only be consumed in the same group. + */ + 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 new file mode 100644 index 0000000..798162c --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/MQPullConsumerScheduleService.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.client.consumer; + +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.ThreadFactoryImpl; +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; + +/** + * 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 = LoggerFactory.getLogger(MQPullConsumerScheduleService.class); + private final MessageQueueListener messageQueueListener = new MessageQueueListenerImpl(); + private final ConcurrentMap taskTable = + new ConcurrentHashMap<>(); + private DefaultMQPullConsumer defaultMQPullConsumer; + private int pullThreadNums = 20; + private ConcurrentMap callbackTable = + new ConcurrentHashMap<>(); + private ScheduledThreadPoolExecutor scheduledThreadPoolExecutor; + + public MQPullConsumerScheduleService(final String consumerGroup) { + this.defaultMQPullConsumer = new DefaultMQPullConsumer(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()) { + Entry next = it.next(); + if (next.getKey().getTopic().equals(topic)) { + if (!mqNewSet.contains(next.getKey())) { + next.getValue().setCancelled(true); + it.remove(); + } + } + } + + for (MessageQueue mq : mqNewSet) { + if (!this.taskTable.containsKey(mq)) { + PullTaskImpl command = new PullTaskImpl(mq); + this.taskTable.put(mq, command); + this.scheduledThreadPoolExecutor.schedule(command, 0, TimeUnit.MILLISECONDS); + + } + } + } + + public void start() throws MQClientException { + final String group = this.defaultMQPullConsumer.getConsumerGroup(); + this.scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor( + this.pullThreadNums, + new ThreadFactoryImpl("PullMsgThread-" + group) + ); + + this.defaultMQPullConsumer.setMessageQueueListener(this.messageQueueListener); + + this.defaultMQPullConsumer.start(); + + log.info("MQPullConsumerScheduleService start OK, {} {}", + this.defaultMQPullConsumer.getConsumerGroup(), this.callbackTable); + } + + public void registerPullTaskCallback(final String topic, final PullTaskCallback callback) { + this.callbackTable.put(NamespaceUtil.wrapNamespace(this.defaultMQPullConsumer.getNamespace(), topic), callback); + this.defaultMQPullConsumer.registerMessageQueueListener(topic, null); + } + + public void shutdown() { + if (this.scheduledThreadPoolExecutor != null) { + this.scheduledThreadPoolExecutor.shutdown(); + } + + if (this.defaultMQPullConsumer != null) { + this.defaultMQPullConsumer.shutdown(); + } + } + + public ConcurrentMap getCallbackTable() { + return callbackTable; + } + + public void setCallbackTable(ConcurrentHashMap callbackTable) { + this.callbackTable = callbackTable; + } + + public int getPullThreadNums() { + return pullThreadNums; + } + + public void setPullThreadNums(int pullThreadNums) { + this.pullThreadNums = pullThreadNums; + } + + public DefaultMQPullConsumer getDefaultMQPullConsumer() { + return defaultMQPullConsumer; + } + + public void setDefaultMQPullConsumer(DefaultMQPullConsumer defaultMQPullConsumer) { + this.defaultMQPullConsumer = defaultMQPullConsumer; + } + + public MessageModel getMessageModel() { + return this.defaultMQPullConsumer.getMessageModel(); + } + + public void setMessageModel(MessageModel messageModel) { + this.defaultMQPullConsumer.setMessageModel(messageModel); + } + + class MessageQueueListenerImpl implements MessageQueueListener { + @Override + public void messageQueueChanged(String topic, Set mqAll, Set mqDivided) { + MessageModel messageModel = + MQPullConsumerScheduleService.this.defaultMQPullConsumer.getMessageModel(); + switch (messageModel) { + case BROADCASTING: + MQPullConsumerScheduleService.this.putTask(topic, mqAll); + break; + case CLUSTERING: + MQPullConsumerScheduleService.this.putTask(topic, mqDivided); + break; + default: + break; + } + } + } + + public class PullTaskImpl implements Runnable { + private final MessageQueue messageQueue; + private volatile boolean cancelled = false; + + public PullTaskImpl(final MessageQueue messageQueue) { + this.messageQueue = messageQueue; + } + + @Override + public void run() { + String topic = this.messageQueue.getTopic(); + if (!this.isCancelled()) { + PullTaskCallback pullTaskCallback = + MQPullConsumerScheduleService.this.callbackTable.get(topic); + if (pullTaskCallback != null) { + final PullTaskContext context = new PullTaskContext(); + context.setPullConsumer(MQPullConsumerScheduleService.this.defaultMQPullConsumer); + try { + pullTaskCallback.doPullTask(this.messageQueue, context); + } catch (Throwable e) { + context.setPullNextDelayTimeMillis(1000); + log.error("doPullTask Exception", e); + } + + if (!this.isCancelled()) { + MQPullConsumerScheduleService.this.scheduledThreadPoolExecutor.schedule(this, + context.getPullNextDelayTimeMillis(), TimeUnit.MILLISECONDS); + } else { + log.warn("The Pull Task is cancelled after doPullTask, {}", messageQueue); + } + } else { + log.warn("Pull Task Callback not exist , {}", topic); + } + } else { + log.warn("The Pull Task is cancelled, {}", messageQueue); + } + } + + public boolean isCancelled() { + return cancelled; + } + + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + + public MessageQueue getMessageQueue() { + return messageQueue; + } + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/MQPushConsumer.java b/client/src/main/java/org/apache/rocketmq/client/consumer/MQPushConsumer.java new file mode 100644 index 0000000..bc6d328 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/MQPushConsumer.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.consumer; + +import org.apache.rocketmq.client.consumer.listener.MessageListener; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; +import org.apache.rocketmq.client.exception.MQClientException; + +/** + * Push consumer + */ +public interface MQPushConsumer extends MQConsumer { + /** + * Start the consumer + */ + void start() throws MQClientException; + + /** + * Shutdown the consumer + */ + void shutdown(); + + /** + * Register the message listener + */ + @Deprecated + void registerMessageListener(MessageListener messageListener); + + void registerMessageListener(final MessageListenerConcurrently messageListener); + + void registerMessageListener(final MessageListenerOrderly messageListener); + + /** + * Subscribe some topic + * + * @param subExpression subscription expression.it only support or operation such as "tag1 || tag2 || tag3"
if + * null or * expression,meaning subscribe + * all + */ + void subscribe(final String topic, final String subExpression) throws MQClientException; + + /** + * This method will be removed in the version 5.0.0,because filterServer was removed,and method subscribe(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; + + /** + * Subscribe some topic with selector. + *

+ * This interface also has the ability of {@link #subscribe(String, String)}, + * and, support other message selection, such as {@link org.apache.rocketmq.common.filter.ExpressionType#SQL92}. + *

+ *

+ *

+ * Choose Tag: {@link MessageSelector#byTag(java.lang.String)} + *

+ *

+ *

+ * Choose SQL92: {@link MessageSelector#bySql(java.lang.String)} + *

+ * + * @param selector message selector({@link MessageSelector}), can be null. + */ + void subscribe(final String topic, final MessageSelector selector) throws MQClientException; + + /** + * Unsubscribe consumption some topic + * + * @param topic message topic + */ + void unsubscribe(final String topic); + + /** + * Update the consumer thread pool size Dynamically + */ + void updateCorePoolSize(int corePoolSize); + + /** + * Suspend the consumption + */ + void suspend(); + + /** + * Resume the consumption + */ + void resume(); +} 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 new file mode 100644 index 0000000..74510f4 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/MessageQueueListener.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.client.consumer; + +import java.util.Set; +import org.apache.rocketmq.common.message.MessageQueue; + +/** + * A MessageQueueListener is implemented by the application and may be specified when a message queue changed + */ +public interface MessageQueueListener { + /** + * @param topic message topic + * @param mqAll all queues in this message topic + * @param mqAssigned collection of queues, assigned to the current consumer + */ + 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 new file mode 100644 index 0000000..236968e --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/MessageSelector.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.client.consumer; + +import org.apache.rocketmq.common.filter.ExpressionType; + +/** + * Message selector: select message at server. + *

+ * Now, support: + *

  • Tag: {@link org.apache.rocketmq.common.filter.ExpressionType#TAG} + *
  • + *
  • SQL92: {@link org.apache.rocketmq.common.filter.ExpressionType#SQL92} + *
  • + *

    + */ +public class MessageSelector { + + /** + * @see org.apache.rocketmq.common.filter.ExpressionType + */ + private String type; + + /** + * expression content. + */ + private String expression; + + private MessageSelector(String type, String expression) { + this.type = type; + this.expression = expression; + } + + /** + * Use SQL92 to select message. + * + * @param sql if null or empty, will be treated as select all message. + */ + public static MessageSelector bySql(String sql) { + return new MessageSelector(ExpressionType.SQL92, sql); + } + + /** + * Use tag to select message. + * + * @param tag if null or empty or "*", will be treated as select all message. + */ + public static MessageSelector byTag(String tag) { + return new MessageSelector(ExpressionType.TAG, tag); + } + + public String getExpressionType() { + return type; + } + + public String getExpression() { + return expression; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/NotifyResult.java b/client/src/main/java/org/apache/rocketmq/client/consumer/NotifyResult.java new file mode 100644 index 0000000..4bd8b28 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/NotifyResult.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.client.consumer; + +public class NotifyResult { + private boolean hasMsg; + private boolean pollingFull; + + public boolean isHasMsg() { + return hasMsg; + } + + public boolean isPollingFull() { + return pollingFull; + } + + public void setHasMsg(boolean hasMsg) { + this.hasMsg = hasMsg; + } + + public void setPollingFull(boolean pollingFull) { + this.pollingFull = pollingFull; + } + + @Override public String toString() { + return "NotifyResult{" + + "hasMsg=" + hasMsg + + ", pollingFull=" + pollingFull + + '}'; + } +} 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 0000000..4932e74 --- /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 0000000..6423e90 --- /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 0000000..57fbe67 --- /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 release + */ + 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/PullCallback.java b/client/src/main/java/org/apache/rocketmq/client/consumer/PullCallback.java new file mode 100644 index 0000000..56ce94e --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/PullCallback.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 pulling interface + */ +public interface PullCallback { + void onSuccess(final PullResult pullResult); + + void onException(final Throwable e); +} 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 new file mode 100644 index 0000000..d887542 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/PullResult.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.client.consumer; + +import java.util.List; +import org.apache.rocketmq.common.message.MessageExt; + +public class PullResult { + private final PullStatus pullStatus; + private final long nextBeginOffset; + private final long minOffset; + private final long maxOffset; + private List msgFoundList; + + + public PullResult(PullStatus pullStatus, long nextBeginOffset, long minOffset, long maxOffset, + List msgFoundList) { + super(); + this.pullStatus = pullStatus; + this.nextBeginOffset = nextBeginOffset; + this.minOffset = minOffset; + this.maxOffset = maxOffset; + this.msgFoundList = msgFoundList; + } + + public PullStatus getPullStatus() { + return pullStatus; + } + + public long getNextBeginOffset() { + return nextBeginOffset; + } + + public long getMinOffset() { + return minOffset; + } + + public long getMaxOffset() { + return maxOffset; + } + + public List getMsgFoundList() { + return msgFoundList; + } + + public void setMsgFoundList(List msgFoundList) { + this.msgFoundList = msgFoundList; + } + + @Override + public String toString() { + return "PullResult [pullStatus=" + pullStatus + ", nextBeginOffset=" + nextBeginOffset + + ", minOffset=" + minOffset + ", maxOffset=" + maxOffset + ", msgFoundList=" + + (msgFoundList == null ? 0 : msgFoundList.size()) + "]"; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/PullStatus.java b/client/src/main/java/org/apache/rocketmq/client/consumer/PullStatus.java new file mode 100644 index 0000000..2f75b6d --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/PullStatus.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.client.consumer; + +public enum PullStatus { + /** + * Founded + */ + FOUND, + /** + * No new message can be pull + */ + NO_NEW_MSG, + /** + * Filtering results can not match + */ + NO_MATCHED_MSG, + /** + * Illegal offset,may be too big or too small + */ + OFFSET_ILLEGAL +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/PullTaskCallback.java b/client/src/main/java/org/apache/rocketmq/client/consumer/PullTaskCallback.java new file mode 100644 index 0000000..3853b44 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/PullTaskCallback.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; + +import org.apache.rocketmq.common.message.MessageQueue; + +public interface PullTaskCallback { + void doPullTask(final MessageQueue mq, final PullTaskContext context); +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/PullTaskContext.java b/client/src/main/java/org/apache/rocketmq/client/consumer/PullTaskContext.java new file mode 100644 index 0000000..766e6b3 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/PullTaskContext.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.consumer; + +public class PullTaskContext { + + private int pullNextDelayTimeMillis = 200; + + private MQPullConsumer pullConsumer; + + public int getPullNextDelayTimeMillis() { + return pullNextDelayTimeMillis; + } + + public void setPullNextDelayTimeMillis(int pullNextDelayTimeMillis) { + this.pullNextDelayTimeMillis = pullNextDelayTimeMillis; + } + + public MQPullConsumer getPullConsumer() { + return pullConsumer; + } + + public void setPullConsumer(MQPullConsumer pullConsumer) { + this.pullConsumer = pullConsumer; + } +} 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 0000000..fa6fd13 --- /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/ConsumeConcurrentlyContext.java b/client/src/main/java/org/apache/rocketmq/client/consumer/listener/ConsumeConcurrentlyContext.java new file mode 100644 index 0000000..97cc020 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/listener/ConsumeConcurrentlyContext.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.client.consumer.listener; + +import org.apache.rocketmq.common.message.MessageQueue; + +/** + * Consumer concurrent consumption context + */ +public class ConsumeConcurrentlyContext { + private final MessageQueue messageQueue; + /** + * Message consume retry strategy
    + * -1,no retry,put into DLQ directly
    + * 0,broker control retry frequency
    + * >0,client control retry frequency + */ + private int delayLevelWhenNextConsume = 0; + private int ackIndex = Integer.MAX_VALUE; + + public ConsumeConcurrentlyContext(MessageQueue messageQueue) { + this.messageQueue = messageQueue; + } + + public int getDelayLevelWhenNextConsume() { + return delayLevelWhenNextConsume; + } + + public void setDelayLevelWhenNextConsume(int delayLevelWhenNextConsume) { + this.delayLevelWhenNextConsume = delayLevelWhenNextConsume; + } + + public MessageQueue getMessageQueue() { + return messageQueue; + } + + public int getAckIndex() { + return ackIndex; + } + + public void setAckIndex(int ackIndex) { + this.ackIndex = ackIndex; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/listener/ConsumeConcurrentlyStatus.java b/client/src/main/java/org/apache/rocketmq/client/consumer/listener/ConsumeConcurrentlyStatus.java new file mode 100644 index 0000000..c34afd2 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/listener/ConsumeConcurrentlyStatus.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.listener; + +public enum ConsumeConcurrentlyStatus { + /** + * Success consumption + */ + CONSUME_SUCCESS, + /** + * Failure consumption,later try to consume + */ + RECONSUME_LATER; +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/listener/ConsumeOrderlyContext.java b/client/src/main/java/org/apache/rocketmq/client/consumer/listener/ConsumeOrderlyContext.java new file mode 100644 index 0000000..fa40725 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/listener/ConsumeOrderlyContext.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.client.consumer.listener; + +import org.apache.rocketmq.common.message.MessageQueue; + +/** + * Consumer Orderly consumption context + */ +public class ConsumeOrderlyContext { + private final MessageQueue messageQueue; + private boolean autoCommit = true; + private long suspendCurrentQueueTimeMillis = -1; + + public ConsumeOrderlyContext(MessageQueue messageQueue) { + this.messageQueue = messageQueue; + } + + public boolean isAutoCommit() { + return autoCommit; + } + + public void setAutoCommit(boolean autoCommit) { + this.autoCommit = autoCommit; + } + + public MessageQueue getMessageQueue() { + return messageQueue; + } + + public long getSuspendCurrentQueueTimeMillis() { + return suspendCurrentQueueTimeMillis; + } + + public void setSuspendCurrentQueueTimeMillis(long suspendCurrentQueueTimeMillis) { + this.suspendCurrentQueueTimeMillis = suspendCurrentQueueTimeMillis; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/listener/ConsumeOrderlyStatus.java b/client/src/main/java/org/apache/rocketmq/client/consumer/listener/ConsumeOrderlyStatus.java new file mode 100644 index 0000000..c154ba7 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/listener/ConsumeOrderlyStatus.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.client.consumer.listener; + +public enum ConsumeOrderlyStatus { + /** + * Success consumption + */ + SUCCESS, + /** + * Rollback consumption(only for binlog consumption) + */ + @Deprecated + ROLLBACK, + /** + * Commit offset(only for binlog consumption) + */ + @Deprecated + COMMIT, + /** + * Suspend current queue a moment + */ + SUSPEND_CURRENT_QUEUE_A_MOMENT; +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/listener/ConsumeReturnType.java b/client/src/main/java/org/apache/rocketmq/client/consumer/listener/ConsumeReturnType.java new file mode 100644 index 0000000..6e563a8 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/listener/ConsumeReturnType.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.client.consumer.listener; + +public enum ConsumeReturnType { + /** + * consume return success + */ + SUCCESS, + /** + * consume timeout ,even if success + */ + TIME_OUT, + /** + * consume throw exception + */ + EXCEPTION, + /** + * consume return null + */ + RETURNNULL, + /** + * consume return failed + */ + FAILED +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/listener/MessageListener.java b/client/src/main/java/org/apache/rocketmq/client/consumer/listener/MessageListener.java new file mode 100644 index 0000000..454cfe2 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/listener/MessageListener.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.listener; + +/** + * A MessageListener object is used to receive asynchronously delivered messages. + */ +public interface MessageListener { +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/listener/MessageListenerConcurrently.java b/client/src/main/java/org/apache/rocketmq/client/consumer/listener/MessageListenerConcurrently.java new file mode 100644 index 0000000..cb39d7e --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/listener/MessageListenerConcurrently.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.client.consumer.listener; + +import java.util.List; +import org.apache.rocketmq.common.message.MessageExt; + +/** + * A MessageListenerConcurrently object is used to receive asynchronously delivered messages concurrently + */ +public interface MessageListenerConcurrently extends MessageListener { + /** + * It is not recommend to throw exception,rather than returning ConsumeConcurrentlyStatus.RECONSUME_LATER if + * consumption failure + * + * @param msgs msgs.size() >= 1
    DefaultMQPushConsumer.consumeMessageBatchMaxSize=1,you can modify here + * @return The consume status + */ + ConsumeConcurrentlyStatus consumeMessage(final List msgs, + final ConsumeConcurrentlyContext context); +} 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 new file mode 100644 index 0000000..419169d --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/listener/MessageListenerOrderly.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.client.consumer.listener; + +import java.util.List; +import org.apache.rocketmq.common.message.MessageExt; + +/** + * A MessageListenerOrderly object is used to receive messages orderly. One queue by one thread + */ +public interface MessageListenerOrderly extends MessageListener { + /** + * It is not recommend to throw exception,rather than returning ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT + * if consumption failure + * + * @param msgs msgs.size() >= 1
    DefaultMQPushConsumer.consumeMessageBatchMaxSize=1,you can modify here + * @return The consume status + */ + ConsumeOrderlyStatus consumeMessage(final List msgs, + final ConsumeOrderlyContext context); +} 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 0000000..50d3cbe --- /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 0000000..08e95dc --- /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 new file mode 100644 index 0000000..6f63a6f --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueAveragely.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.ArrayList; +import java.util.List; +import org.apache.rocketmq.common.message.MessageQueue; + +/** + * Average Hashing queue algorithm + */ +public class AllocateMessageQueueAveragely extends AbstractAllocateMessageQueueStrategy { + + @Override + public List allocate(String consumerGroup, String currentCID, List mqAll, + List cidAll) { + + List result = new ArrayList<>(); + if (!check(consumerGroup, currentCID, mqAll, cidAll)) { + return result; + } + + int index = cidAll.indexOf(currentCID); + int mod = mqAll.size() % cidAll.size(); + int averageSize = + mqAll.size() <= cidAll.size() ? 1 : (mod > 0 && index < mod ? mqAll.size() / cidAll.size() + + 1 : mqAll.size() / cidAll.size()); + int startIndex = (mod > 0 && index < mod) ? index * averageSize : index * averageSize + mod; + int range = Math.min(averageSize, mqAll.size() - startIndex); + for (int i = 0; i < range; i++) { + result.add(mqAll.get(startIndex + i)); + } + return result; + } + + @Override + public String getName() { + return "AVG"; + } +} 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 new file mode 100644 index 0000000..cc618a8 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueAveragelyByCircle.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.client.consumer.rebalance; + +import java.util.ArrayList; +import java.util.List; +import org.apache.rocketmq.common.message.MessageQueue; + +/** + * Cycle average Hashing queue algorithm + */ +public class AllocateMessageQueueAveragelyByCircle extends AbstractAllocateMessageQueueStrategy { + + @Override + public List allocate(String consumerGroup, String currentCID, List mqAll, + List cidAll) { + + List result = new ArrayList<>(); + if (!check(consumerGroup, currentCID, mqAll, cidAll)) { + return result; + } + + int index = cidAll.indexOf(currentCID); + for (int i = index; i < mqAll.size(); i++) { + if (i % cidAll.size() == index) { + result.add(mqAll.get(i)); + } + } + return result; + } + + @Override + public String getName() { + return "AVG_BY_CIRCLE"; + } +} 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 new file mode 100644 index 0000000..5866e95 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueByConfig.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.client.consumer.rebalance; + +import java.util.List; +import org.apache.rocketmq.common.message.MessageQueue; + +public class AllocateMessageQueueByConfig extends AbstractAllocateMessageQueueStrategy { + private List messageQueueList; + + @Override + public List allocate(String consumerGroup, String currentCID, List mqAll, + List cidAll) { + return this.messageQueueList; + } + + @Override + public String getName() { + return "CONFIG"; + } + + public List getMessageQueueList() { + return messageQueueList; + } + + public void setMessageQueueList(List messageQueueList) { + this.messageQueueList = messageQueueList; + } +} 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 new file mode 100644 index 0000000..4a2ba88 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueByMachineRoom.java @@ -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. + */ +package org.apache.rocketmq.client.consumer.rebalance; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.apache.rocketmq.common.message.MessageQueue; + +/** + * Computer room Hashing queue algorithm, such as Alipay logic room + */ +public class AllocateMessageQueueByMachineRoom extends AbstractAllocateMessageQueueStrategy { + private Set consumeridcs; + + @Override + public List allocate(String consumerGroup, String currentCID, List mqAll, + List cidAll) { + + 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<>(); + for (MessageQueue mq : mqAll) { + String[] temp = mq.getBrokerName().split("@"); + if (temp.length == 2 && consumeridcs.contains(temp[0])) { + premqAll.add(mq); + } + } + + int mod = premqAll.size() / cidAll.size(); + int rem = premqAll.size() % cidAll.size(); + int startIndex = mod * currentIndex; + int endIndex = startIndex + mod; + for (int i = startIndex; i < endIndex; i++) { + result.add(premqAll.get(i)); + } + if (rem > currentIndex) { + result.add(premqAll.get(currentIndex + mod * cidAll.size())); + } + return result; + } + + @Override + public String getName() { + return "MACHINE_ROOM"; + } + + public Set getConsumeridcs() { + return consumeridcs; + } + + public void setConsumeridcs(Set consumeridcs) { + this.consumeridcs = consumeridcs; + } +} 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 new file mode 100644 index 0000000..eea19ed --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueConsistentHash.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.client.consumer.rebalance; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +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; + +/** + * Consistent Hashing queue algorithm + */ +public class AllocateMessageQueueConsistentHash extends AbstractAllocateMessageQueueStrategy { + + private final int virtualNodeCnt; + private final HashFunction customHashFunction; + + public AllocateMessageQueueConsistentHash() { + this(10); + } + + public AllocateMessageQueueConsistentHash(int virtualNodeCnt) { + this(virtualNodeCnt, null); + } + + public AllocateMessageQueueConsistentHash(int virtualNodeCnt, HashFunction customHashFunction) { + if (virtualNodeCnt < 0) { + throw new IllegalArgumentException("illegal virtualNodeCnt :" + virtualNodeCnt); + } + this.virtualNodeCnt = virtualNodeCnt; + this.customHashFunction = customHashFunction; + } + + @Override + public List allocate(String consumerGroup, String currentCID, List mqAll, + List cidAll) { + + List result = new ArrayList<>(); + if (!check(consumerGroup, currentCID, mqAll, cidAll)) { + return result; + } + + 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); + } else { + router = new ConsistentHashRouter<>(cidNodes, virtualNodeCnt); + } + + List results = new ArrayList<>(); + for (MessageQueue mq : mqAll) { + ClientNode clientNode = router.routeNode(mq.toString()); + if (clientNode != null && currentCID.equals(clientNode.getKey())) { + results.add(mq); + } + } + + return results; + + } + + @Override + public String getName() { + return "CONSISTENT_HASH"; + } + + private static class ClientNode implements Node { + private final String clientID; + + public ClientNode(String clientID) { + this.clientID = clientID; + } + + @Override + public String getKey() { + return clientID; + } + } + +} 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 0000000..9db4bd2 --- /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 new file mode 100644 index 0000000..38b0a5b --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/store/LocalFileOffsetStore.java @@ -0,0 +1,276 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.io.File; +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; +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.impl.factory.MQClientInstance; +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.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +/** + * Local storage implementation + */ +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 = LoggerFactory.getLogger(LocalFileOffsetStore.class); + private final MQClientInstance mQClientFactory; + private final String groupName; + private final String storePath; + private ConcurrentMap offsetTable = + new ConcurrentHashMap<>(); + + public LocalFileOffsetStore(MQClientInstance mQClientFactory, String groupName) { + this.mQClientFactory = mQClientFactory; + this.groupName = groupName; + this.storePath = LOCAL_OFFSET_STORE_DIR + File.separator + + this.mQClientFactory.getClientId() + File.separator + + this.groupName + File.separator + + "offsets.json"; + } + + @Override + public void load() throws MQClientException { + OffsetSerializeWrapper offsetSerializeWrapper = this.readLocalOffset(); + if (offsetSerializeWrapper != null && offsetSerializeWrapper.getOffsetTable() != null) { + 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, + mqEntry.getKey(), + offset.get()); + } + } + } + + @Override + public void updateOffset(MessageQueue mq, long offset, boolean increaseOnly) { + if (mq != null) { + ControllableOffset offsetOld = this.offsetTable.get(mq); + if (null == offsetOld) { + offsetOld = this.offsetTable.putIfAbsent(mq, new ControllableOffset(offset)); + } + + if (null != offsetOld) { + if (increaseOnly) { + offsetOld.update(offset, true); + } else { + 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: { + ControllableOffset offset = this.offsetTable.get(mq); + if (offset != null) { + return offset.getOffset(); + } else if (ReadOffsetType.READ_FROM_MEMORY == type) { + return -1; + } + } + case READ_FROM_STORE: { + OffsetSerializeWrapper offsetSerializeWrapper; + try { + offsetSerializeWrapper = this.readLocalOffset(); + } catch (MQClientException e) { + return -1; + } + if (offsetSerializeWrapper != null && offsetSerializeWrapper.getOffsetTable() != null) { + AtomicLong offset = offsetSerializeWrapper.getOffsetTable().get(mq); + if (offset != null) { + this.updateOffset(mq, offset.get(), false); + return offset.get(); + } + } + } + default: + break; + } + } + + return -1; + } + + @Override + public void persistAll(Set mqs) { + if (null == mqs || mqs.isEmpty()) { + return; + } + OffsetSerializeWrapper offsetSerializeWrapper = null; + try { + offsetSerializeWrapper = readLocalOffset(); + } catch (MQClientException e) { + log.error("readLocalOffset exception", e); + return; + } + + if (offsetSerializeWrapper == null) { + offsetSerializeWrapper = new OffsetSerializeWrapper(); + } + for (Map.Entry entry : this.offsetTable.entrySet()) { + if (mqs.contains(entry.getKey())) { + AtomicLong offset = new AtomicLong(entry.getValue().getOffset()); + offsetSerializeWrapper.getOffsetTable().put(entry.getKey(), offset); + } + } + + String jsonString = offsetSerializeWrapper.toJson(true); + if (jsonString != null) { + try { + MixAll.string2File(jsonString, this.storePath); + } catch (IOException e) { + log.error("persistAll consumer offset Exception, " + this.storePath, e); + } + } + } + + @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 + public void updateConsumeOffsetToBroker(final MessageQueue mq, final long offset, final boolean isOneway) + throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + + } + + @Override + public Map cloneOffsetTable(String topic) { + 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().getOffset()); + + } + return cloneOffsetTable; + } + + private OffsetSerializeWrapper readLocalOffset() throws MQClientException { + String content = null; + try { + content = MixAll.file2String(this.storePath); + } catch (IOException e) { + log.warn("Load local offset store file exception", e); + } + if (null == content || content.length() == 0) { + return this.readLocalOffsetBak(); + } else { + OffsetSerializeWrapper offsetSerializeWrapper = null; + try { + offsetSerializeWrapper = + OffsetSerializeWrapper.fromJson(content, OffsetSerializeWrapper.class); + } catch (Exception e) { + log.warn("readLocalOffset Exception, and try to correct", e); + return this.readLocalOffsetBak(); + } + + return offsetSerializeWrapper; + } + } + + private OffsetSerializeWrapper readLocalOffsetBak() throws MQClientException { + String content = null; + try { + content = MixAll.file2String(this.storePath + ".bak"); + } catch (IOException e) { + log.warn("Load local offset store bak file exception", e); + } + if (content != null && content.length() > 0) { + OffsetSerializeWrapper offsetSerializeWrapper = null; + try { + offsetSerializeWrapper = + OffsetSerializeWrapper.fromJson(content, OffsetSerializeWrapper.class); + } catch (Exception e) { + log.warn("readLocalOffset Exception", e); + throw new MQClientException("readLocalOffset Exception, maybe fastjson version too low" + + FAQUrl.suggestTodo(FAQUrl.LOAD_JSON_EXCEPTION), + e); + } + return offsetSerializeWrapper; + } + + return null; + } +} 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 new file mode 100644 index 0000000..85fe0d4 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/store/OffsetSerializeWrapper.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.client.consumer.store; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +/** + * Wrapper class for offset serialization + */ +public class OffsetSerializeWrapper extends RemotingSerializable { + private ConcurrentMap offsetTable = + new ConcurrentHashMap<>(); + + public ConcurrentMap getOffsetTable() { + return offsetTable; + } + + public void setOffsetTable(ConcurrentMap offsetTable) { + this.offsetTable = 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 new file mode 100644 index 0000000..ecceede --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/store/OffsetStore.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.store; + +import java.util.Map; +import java.util.Set; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.exception.RemotingException; + +/** + * Offset store interface + */ +public interface OffsetStore { + /** + * Load + */ + void load() throws MQClientException; + + /** + * Update the offset,store it in memory + */ + 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 + * + * @return The fetched offset + */ + long readOffset(final MessageQueue mq, final ReadOffsetType type); + + /** + * Persist all offsets,may be in local storage or remote name server + */ + void persistAll(final Set mqs); + + /** + * Persist the offset,may be in local storage or remote name server + */ + void persist(final MessageQueue mq); + + /** + * Remove offset + */ + void removeOffset(MessageQueue mq); + + /** + * @return The cloned offset table of given topic + */ + Map cloneOffsetTable(String topic); + + /** + * @param mq + * @param offset + * @param isOneway + */ + void updateConsumeOffsetToBroker(MessageQueue mq, long offset, boolean isOneway) throws RemotingException, + MQBrokerException, InterruptedException, MQClientException; +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/store/ReadOffsetType.java b/client/src/main/java/org/apache/rocketmq/client/consumer/store/ReadOffsetType.java new file mode 100644 index 0000000..1299bf2 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/store/ReadOffsetType.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.client.consumer.store; + +public enum ReadOffsetType { + /** + * From memory + */ + READ_FROM_MEMORY, + /** + * From storage + */ + READ_FROM_STORE, + /** + * From memory,then from storage + */ + MEMORY_FIRST_THEN_STORE; +} 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 new file mode 100644 index 0000000..1a2ffe5 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/store/RemoteBrokerOffsetStore.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.client.consumer.store; + +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.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.common.MixAll; +import org.apache.rocketmq.common.UtilAll; +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; +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 = LoggerFactory.getLogger(RemoteBrokerOffsetStore.class); + private final MQClientInstance mQClientFactory; + private final String groupName; + private ConcurrentMap offsetTable = + new ConcurrentHashMap<>(); + + public RemoteBrokerOffsetStore(MQClientInstance mQClientFactory, String groupName) { + this.mQClientFactory = mQClientFactory; + this.groupName = groupName; + } + + @Override + public void load() { + } + + @Override + public void updateOffset(MessageQueue mq, long offset, boolean increaseOnly) { + if (mq != null) { + ControllableOffset offsetOld = this.offsetTable.get(mq); + if (null == offsetOld) { + offsetOld = this.offsetTable.putIfAbsent(mq, new ControllableOffset(offset)); + } + + if (null != offsetOld) { + if (increaseOnly) { + offsetOld.update(offset, true); + } else { + 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: { + ControllableOffset offset = this.offsetTable.get(mq); + if (offset != null) { + return offset.getOffset(); + } else if (ReadOffsetType.READ_FROM_MEMORY == type) { + return -1; + } + } + case READ_FROM_STORE: { + try { + long brokerOffset = this.fetchConsumeOffsetFromBroker(mq); + this.updateOffset(mq, brokerOffset, false); + return brokerOffset; + } + // No offset in broker + catch (OffsetNotFoundException e) { + return -1; + } + //Other exceptions + catch (Exception e) { + log.warn("fetchConsumeOffsetFromBroker exception, " + mq, e); + return -2; + } + } + default: + break; + } + } + + return -3; + } + + @Override + public void persistAll(Set mqs) { + if (null == mqs || mqs.isEmpty()) + return; + + 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); + } + } + } + + if (!unusedMQ.isEmpty()) { + for (MessageQueue mq : unusedMQ) { + this.offsetTable.remove(mq); + log.info("remove unused mq, {}, {}", mq, this.groupName); + } + } + } + + @Override + public void persist(MessageQueue mq) { + ControllableOffset offset = this.offsetTable.get(mq); + if (offset != null) { + try { + this.updateConsumeOffsetToBroker(mq, offset.getOffset()); + log.info("[persist] Group: {} ClientId: {} updateConsumeOffsetToBroker {} {}", + this.groupName, + this.mQClientFactory.getClientId(), + mq, + offset.getOffset()); + } catch (Exception e) { + log.error("updateConsumeOffsetToBroker exception, " + mq.toString(), e); + } + } + } + + 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 + public Map cloneOffsetTable(String topic) { + 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().getOffset()); + } + return cloneOffsetTable; + } + + /** + * 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 { + updateConsumeOffsetToBroker(mq, offset, true); + } + + /** + * 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.findBrokerAddressInSubscribe(this.mQClientFactory.getBrokerNameFromMessageQueue(mq), MixAll.MASTER_ID, false); + if (null == findBrokerResult) { + this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic()); + findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(this.mQClientFactory.getBrokerNameFromMessageQueue(mq), MixAll.MASTER_ID, false); + } + + if (findBrokerResult != null) { + UpdateConsumerOffsetRequestHeader requestHeader = new UpdateConsumerOffsetRequestHeader(); + requestHeader.setTopic(mq.getTopic()); + requestHeader.setConsumerGroup(this.groupName); + requestHeader.setQueueId(mq.getQueueId()); + requestHeader.setCommitOffset(offset); + requestHeader.setBrokerName(mq.getBrokerName()); + + if (isOneway) { + this.mQClientFactory.getMQClientAPIImpl().updateConsumerOffsetOneway( + findBrokerResult.getBrokerAddr(), requestHeader, 1000 * 5); + } else { + this.mQClientFactory.getMQClientAPIImpl().updateConsumerOffset( + findBrokerResult.getBrokerAddr(), requestHeader, 1000 * 5); + } + } else { + throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null); + } + } + + private long fetchConsumeOffsetFromBroker(MessageQueue mq) throws RemotingException, MQBrokerException, + InterruptedException, MQClientException { + FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(this.mQClientFactory.getBrokerNameFromMessageQueue(mq), MixAll.MASTER_ID, true); + if (null == findBrokerResult) { + this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic()); + findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(this.mQClientFactory.getBrokerNameFromMessageQueue(mq), MixAll.MASTER_ID, false); + } + + if (findBrokerResult != null) { + QueryConsumerOffsetRequestHeader requestHeader = new QueryConsumerOffsetRequestHeader(); + 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); + } else { + throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null); + } + } +} 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 new file mode 100644 index 0000000..7870ff1 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/exception/MQBrokerException.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.client.exception; + +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.help.FAQUrl; + +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)); + 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() { + return responseCode; + } + + 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 new file mode 100644 index 0000000..9bbcce2 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/exception/MQClientException.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.client.exception; + +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.help.FAQUrl; + +public class MQClientException extends Exception { + private static final long serialVersionUID = -5758410930844185841L; + private int responseCode; + private String errorMessage; + + public MQClientException(String errorMessage, Throwable cause) { + super(FAQUrl.attachDefaultURL(errorMessage), cause); + this.responseCode = -1; + this.errorMessage = errorMessage; + } + + public MQClientException(int responseCode, String errorMessage) { + super(FAQUrl.attachDefaultURL("CODE: " + UtilAll.responseCode2String(responseCode) + " DESC: " + + errorMessage)); + this.responseCode = responseCode; + 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; + } + + public MQClientException 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/exception/OffsetNotFoundException.java b/client/src/main/java/org/apache/rocketmq/client/exception/OffsetNotFoundException.java new file mode 100644 index 0000000..e73bbbf --- /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 0000000..2d756ec --- /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/CheckForbiddenContext.java b/client/src/main/java/org/apache/rocketmq/client/hook/CheckForbiddenContext.java new file mode 100644 index 0000000..9f430be --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/hook/CheckForbiddenContext.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.client.hook; + +import org.apache.rocketmq.client.impl.CommunicationMode; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageQueue; + +public class CheckForbiddenContext { + private String nameSrvAddr; + private String group; + private Message message; + private MessageQueue mq; + private String brokerAddr; + private CommunicationMode communicationMode; + private SendResult sendResult; + private Exception exception; + private Object arg; + private boolean unitMode = false; + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public Message getMessage() { + return message; + } + + public void setMessage(Message message) { + this.message = message; + } + + public MessageQueue getMq() { + return mq; + } + + public void setMq(MessageQueue mq) { + this.mq = mq; + } + + public String getBrokerAddr() { + return brokerAddr; + } + + public void setBrokerAddr(String brokerAddr) { + this.brokerAddr = brokerAddr; + } + + public CommunicationMode getCommunicationMode() { + return communicationMode; + } + + public void setCommunicationMode(CommunicationMode communicationMode) { + this.communicationMode = communicationMode; + } + + public SendResult getSendResult() { + return sendResult; + } + + public void setSendResult(SendResult sendResult) { + this.sendResult = sendResult; + } + + public Exception getException() { + return exception; + } + + public void setException(Exception exception) { + this.exception = exception; + } + + public Object getArg() { + return arg; + } + + public void setArg(Object arg) { + this.arg = arg; + } + + public boolean isUnitMode() { + return unitMode; + } + + public void setUnitMode(boolean isUnitMode) { + this.unitMode = isUnitMode; + } + + public String getNameSrvAddr() { + return nameSrvAddr; + } + + public void setNameSrvAddr(String nameSrvAddr) { + this.nameSrvAddr = nameSrvAddr; + } + + @Override + public String toString() { + return "SendMessageContext [nameSrvAddr=" + nameSrvAddr + ", group=" + group + ", message=" + message + + ", mq=" + mq + ", brokerAddr=" + brokerAddr + ", communicationMode=" + communicationMode + + ", sendResult=" + sendResult + ", exception=" + exception + ", unitMode=" + unitMode + + ", arg=" + arg + "]"; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/hook/CheckForbiddenHook.java b/client/src/main/java/org/apache/rocketmq/client/hook/CheckForbiddenHook.java new file mode 100644 index 0000000..0631e10 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/hook/CheckForbiddenHook.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.hook; + +import org.apache.rocketmq.client.exception.MQClientException; + +public interface CheckForbiddenHook { + String hookName(); + + void checkForbidden(final CheckForbiddenContext context) throws MQClientException; +} 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 new file mode 100644 index 0000000..94633ce --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/hook/ConsumeMessageContext.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.client.hook; + +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; + +public class ConsumeMessageContext { + private String consumerGroup; + private List msgList; + private MessageQueue mq; + private boolean success; + private String status; + private Object mqTraceContext; + private Map props; + private String namespace; + private AccessChannel accessChannel; + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public List getMsgList() { + return msgList; + } + + public void setMsgList(List msgList) { + this.msgList = msgList; + } + + public MessageQueue getMq() { + return mq; + } + + public void setMq(MessageQueue mq) { + this.mq = mq; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public Object getMqTraceContext() { + return mqTraceContext; + } + + public void setMqTraceContext(Object mqTraceContext) { + this.mqTraceContext = mqTraceContext; + } + + public Map getProps() { + return props; + } + + public void setProps(Map props) { + this.props = props; + } + + public String getStatus() { + return status; + } + + 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/ConsumeMessageHook.java b/client/src/main/java/org/apache/rocketmq/client/hook/ConsumeMessageHook.java new file mode 100644 index 0000000..5ec5f7f --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/hook/ConsumeMessageHook.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.client.hook; + +public interface ConsumeMessageHook { + String hookName(); + + void consumeMessageBefore(final ConsumeMessageContext context); + + void consumeMessageAfter(final ConsumeMessageContext context); +} 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 0000000..5271ade --- /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 0000000..834cb27 --- /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/FilterMessageContext.java b/client/src/main/java/org/apache/rocketmq/client/hook/FilterMessageContext.java new file mode 100644 index 0000000..c470153 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/hook/FilterMessageContext.java @@ -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. + */ +package org.apache.rocketmq.client.hook; + +import java.util.List; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; + +public class FilterMessageContext { + private String consumerGroup; + private List msgList; + private MessageQueue mq; + private Object arg; + private boolean unitMode; + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public List getMsgList() { + return msgList; + } + + public void setMsgList(List msgList) { + this.msgList = msgList; + } + + public MessageQueue getMq() { + return mq; + } + + public void setMq(MessageQueue mq) { + this.mq = mq; + } + + public Object getArg() { + return arg; + } + + public void setArg(Object arg) { + this.arg = arg; + } + + public boolean isUnitMode() { + return unitMode; + } + + public void setUnitMode(boolean isUnitMode) { + this.unitMode = isUnitMode; + } + + @Override + public String toString() { + return "ConsumeMessageContext [consumerGroup=" + consumerGroup + ", msgList=" + msgList + ", mq=" + + mq + ", arg=" + arg + "]"; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/hook/FilterMessageHook.java b/client/src/main/java/org/apache/rocketmq/client/hook/FilterMessageHook.java new file mode 100644 index 0000000..84e2d89 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/hook/FilterMessageHook.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 FilterMessageHook { + String hookName(); + + void filterMessage(final FilterMessageContext 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 new file mode 100644 index 0000000..e970d81 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/hook/SendMessageContext.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.client.hook; + +import java.util.Map; +import org.apache.rocketmq.client.impl.CommunicationMode; +import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl; +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.common.message.MessageType; + +public class SendMessageContext { + private String producerGroup; + private Message message; + private MessageQueue mq; + private String brokerAddr; + private String bornHost; + private CommunicationMode communicationMode; + private SendResult sendResult; + private Exception exception; + private Object mqTraceContext; + private Map props; + private DefaultMQProducerImpl producer; + private MessageType msgType = MessageType.Normal_Msg; + private String namespace; + + public MessageType getMsgType() { + return msgType; + } + + public void setMsgType(final MessageType msgType) { + this.msgType = msgType; + } + + public DefaultMQProducerImpl getProducer() { + return producer; + } + + public void setProducer(final DefaultMQProducerImpl producer) { + this.producer = producer; + } + + 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 MessageQueue getMq() { + return mq; + } + + public void setMq(MessageQueue mq) { + this.mq = mq; + } + + public String getBrokerAddr() { + return brokerAddr; + } + + public void setBrokerAddr(String brokerAddr) { + this.brokerAddr = brokerAddr; + } + + public CommunicationMode getCommunicationMode() { + return communicationMode; + } + + public void setCommunicationMode(CommunicationMode communicationMode) { + this.communicationMode = communicationMode; + } + + public SendResult getSendResult() { + return sendResult; + } + + public void setSendResult(SendResult sendResult) { + this.sendResult = sendResult; + } + + public Exception getException() { + return exception; + } + + public void setException(Exception exception) { + this.exception = exception; + } + + public Object getMqTraceContext() { + return mqTraceContext; + } + + public void setMqTraceContext(Object mqTraceContext) { + this.mqTraceContext = mqTraceContext; + } + + public Map getProps() { + return props; + } + + public void setProps(Map props) { + this.props = props; + } + + public String getBornHost() { + return bornHost; + } + + 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/hook/SendMessageHook.java b/client/src/main/java/org/apache/rocketmq/client/hook/SendMessageHook.java new file mode 100644 index 0000000..c55530a --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/hook/SendMessageHook.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.client.hook; + +public interface SendMessageHook { + String hookName(); + + void sendMessageBefore(final SendMessageContext context); + + void sendMessageAfter(final SendMessageContext context); +} 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 new file mode 100644 index 0000000..e46c651 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/ClientRemotingProcessor.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.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.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.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.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 logger = LoggerFactory.getLogger(ClientRemotingProcessor.class); + private final MQClientInstance mqClientFactory; + + public ClientRemotingProcessor(final MQClientInstance mqClientFactory) { + this.mqClientFactory = mqClientFactory; + } + + @Override + public RemotingCommand processRequest(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + switch (request.getCode()) { + case RequestCode.CHECK_TRANSACTION_STATE: + return this.checkTransactionState(ctx, request); + case RequestCode.NOTIFY_CONSUMER_IDS_CHANGED: + return this.notifyConsumerIdsChanged(ctx, request); + case RequestCode.RESET_CONSUMER_CLIENT_OFFSET: + return this.resetOffset(ctx, request); + case RequestCode.GET_CONSUMER_STATUS_FROM_CLIENT: + return this.getConsumeStatus(ctx, request); + + case RequestCode.GET_CONSUMER_RUNNING_INFO: + return this.getConsumerRunningInfo(ctx, request); + + case RequestCode.CONSUME_MESSAGE_DIRECTLY: + return this.consumeMessageDirectly(ctx, request); + + case RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT: + return this.receiveReplyMessage(ctx, request); + default: + break; + } + return null; + } + + @Override + public boolean rejectRequest() { + return false; + } + + public RemotingCommand checkTransactionState(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final CheckTransactionStateRequestHeader requestHeader = + (CheckTransactionStateRequestHeader) request.decodeCommandCustomHeader(CheckTransactionStateRequestHeader.class); + 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); + if (producer != null) { + final String addr = RemotingHelper.parseChannelRemoteAddr(ctx.channel()); + producer.checkTransactionState(addr, messageExt, requestHeader); + } else { + logger.debug("checkTransactionState, pick producer by group[{}] failed", group); + } + } else { + logger.warn("checkTransactionState, pick producer group failed"); + } + } else { + logger.warn("checkTransactionState, decode message failed"); + } + + return null; + } + + public RemotingCommand notifyConsumerIdsChanged(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + try { + final NotifyConsumerIdsChangedRequestHeader requestHeader = + (NotifyConsumerIdsChangedRequestHeader) request.decodeCommandCustomHeader(NotifyConsumerIdsChangedRequestHeader.class); + logger.info("receive broker's notification[{}], the consumer group: {} changed, rebalance immediately", + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), + requestHeader.getConsumerGroup()); + this.mqClientFactory.rebalanceImmediately(); + } catch (Exception e) { + logger.error("notifyConsumerIdsChanged exception", UtilAll.exceptionSimpleDesc(e)); + } + return null; + } + + public RemotingCommand resetOffset(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final ResetOffsetRequestHeader requestHeader = + (ResetOffsetRequestHeader) request.decodeCommandCustomHeader(ResetOffsetRequestHeader.class); + 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<>(); + if (request.getBody() != null) { + ResetOffsetBody body = ResetOffsetBody.decode(request.getBody(), ResetOffsetBody.class); + offsetTable = body.getOffsetTable(); + } + this.mqClientFactory.resetOffset(requestHeader.getTopic(), requestHeader.getGroup(), offsetTable); + return null; + } + + @Deprecated + public RemotingCommand getConsumeStatus(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + final GetConsumerStatusRequestHeader requestHeader = + (GetConsumerStatusRequestHeader) request.decodeCommandCustomHeader(GetConsumerStatusRequestHeader.class); + + Map offsetTable = this.mqClientFactory.getConsumerStatus(requestHeader.getTopic(), requestHeader.getGroup()); + GetConsumerStatusBody body = new GetConsumerStatusBody(); + body.setMessageQueueTable(offsetTable); + response.setBody(body.encode()); + response.setCode(ResponseCode.SUCCESS); + return response; + } + + private RemotingCommand getConsumerRunningInfo(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + final GetConsumerRunningInfoRequestHeader requestHeader = + (GetConsumerRunningInfoRequestHeader) request.decodeCommandCustomHeader(GetConsumerRunningInfoRequestHeader.class); + + ConsumerRunningInfo consumerRunningInfo = this.mqClientFactory.consumerRunningInfo(requestHeader.getConsumerGroup()); + if (null != consumerRunningInfo) { + if (requestHeader.isJstackEnable()) { + Map map = Thread.getAllStackTraces(); + String jstack = UtilAll.jstack(map); + consumerRunningInfo.setJstack(jstack); + } + + response.setCode(ResponseCode.SUCCESS); + response.setBody(consumerRunningInfo.encode()); + } else { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(String.format("The Consumer Group <%s> not exist in this consumer", requestHeader.getConsumerGroup())); + } + + return response; + } + + private RemotingCommand consumeMessageDirectly(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + final ConsumeMessageDirectlyResultRequestHeader requestHeader = + (ConsumeMessageDirectlyResultRequestHeader) request + .decodeCommandCustomHeader(ConsumeMessageDirectlyResultRequestHeader.class); + + final MessageExt msg = MessageDecoder.clientDecode(ByteBuffer.wrap(request.getBody()), true); + + ConsumeMessageDirectlyResult result = + this.mqClientFactory.consumeMessageDirectly(msg, requestHeader.getConsumerGroup(), requestHeader.getBrokerName()); + + if (null != result) { + response.setCode(ResponseCode.SUCCESS); + response.setBody(result.encode()); + } else { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(String.format("The Consumer Group <%s> not exist in this consumer", requestHeader.getConsumerGroup())); + } + + 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("receive reply message, but not matched any request, CorrelationId: {} , reply from host: {}", + correlationId, bornHost); + } + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/CommunicationMode.java b/client/src/main/java/org/apache/rocketmq/client/impl/CommunicationMode.java new file mode 100644 index 0000000..7ccb27d --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/CommunicationMode.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; + +public enum CommunicationMode { + SYNC, + ASYNC, + ONEWAY, +} diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/FindBrokerResult.java b/client/src/main/java/org/apache/rocketmq/client/impl/FindBrokerResult.java new file mode 100644 index 0000000..4367a4c --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/FindBrokerResult.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.impl; + +public class FindBrokerResult { + private final String brokerAddr; + private final boolean slave; + private final int brokerVersion; + + public FindBrokerResult(String brokerAddr, boolean slave) { + this.brokerAddr = brokerAddr; + this.slave = slave; + this.brokerVersion = 0; + } + + public FindBrokerResult(String brokerAddr, boolean slave, int brokerVersion) { + this.brokerAddr = brokerAddr; + this.slave = slave; + this.brokerVersion = brokerVersion; + } + + public String getBrokerAddr() { + return brokerAddr; + } + + public boolean isSlave() { + return slave; + } + + public int getBrokerVersion() { + return brokerVersion; + } +} 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 new file mode 100644 index 0000000..c1e3ee3 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/MQAdminImpl.java @@ -0,0 +1,489 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; + +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.commons.lang3.StringUtils; +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.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.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.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.InvokeCallback; +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.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 static final Logger log = LoggerFactory.getLogger(MQAdminImpl.class); + private final MQClientInstance mQClientFactory; + private long timeoutMillis = 6000; + + public MQAdminImpl(MQClientInstance mQClientFactory) { + this.mQClientFactory = mQClientFactory; + } + + public long getTimeoutMillis() { + return timeoutMillis; + } + + public void setTimeoutMillis(long timeoutMillis) { + this.timeoutMillis = timeoutMillis; + } + + public void createTopic(String key, String newTopic, int queueNum) throws MQClientException { + createTopic(key, newTopic, queueNum, 0, null); + } + + 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()) { + Collections.sort(brokerDataList); + + boolean createOKAtLeastOnce = false; + MQClientException exception = null; + + StringBuilder orderTopicString = new StringBuilder(); + + for (BrokerData brokerData : brokerDataList) { + String addr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID); + if (addr != null) { + TopicConfig topicConfig = new TopicConfig(newTopic); + topicConfig.setReadQueueNums(queueNum); + topicConfig.setWriteQueueNums(queueNum); + topicConfig.setTopicSysFlag(topicSysFlag); + topicConfig.setAttributes(attributes); + + boolean createOK = false; + for (int i = 0; i < 5; i++) { + try { + this.mQClientFactory.getMQClientAPIImpl().createTopic(addr, key, topicConfig, timeoutMillis); + createOK = true; + createOKAtLeastOnce = true; + break; + } catch (Exception e) { + if (4 == i) { + exception = new MQClientException("create topic to broker exception", e); + } + } + } + + if (createOK) { + orderTopicString.append(brokerData.getBrokerName()); + orderTopicString.append(":"); + orderTopicString.append(queueNum); + orderTopicString.append(";"); + } + } + } + + if (exception != null && !createOKAtLeastOnce) { + throw exception; + } + } else { + throw new MQClientException("Not found broker, maybe key is wrong", null); + } + } catch (Exception e) { + throw new MQClientException("create new topic failed", e); + } + } + + public List fetchPublishMessageQueues(String topic) throws MQClientException { + try { + TopicRouteData topicRouteData = this.mQClientFactory.getMQClientAPIImpl().getTopicRouteInfoFromNameServer(topic, timeoutMillis); + if (topicRouteData != null) { + TopicPublishInfo topicPublishInfo = MQClientInstance.topicRouteData2TopicPublishInfo(topic, topicRouteData); + if (topicPublishInfo != null && topicPublishInfo.ok()) { + return parsePublishMessageQueues(topicPublishInfo.getMessageQueueList()); + } + } + } catch (Exception e) { + throw new MQClientException("Can not find Message Queue for this topic, " + topic, e); + } + + 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); + if (topicRouteData != null) { + Set mqList = MQClientInstance.topicRouteData2TopicSubscribeInfo(topic, topicRouteData); + if (!mqList.isEmpty()) { + return mqList; + } else { + throw new MQClientException("Can not find Message Queue for this topic, " + topic + " Namesrv return empty", null); + } + } + } catch (Exception e) { + throw new MQClientException( + "Can not find Message Queue for this topic, " + topic + FAQUrl.suggestTodo(FAQUrl.MQLIST_NOT_EXIST), + e); + } + + throw new MQClientException("Unknown why, Can not find Message Queue for this topic, " + topic, null); + } + + public long searchOffset(MessageQueue mq, long timestamp) throws MQClientException { + // 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(this.mQClientFactory.getBrokerNameFromMessageQueue(mq)); + } + + if (brokerAddr != null) { + try { + return this.mQClientFactory.getMQClientAPIImpl().searchOffset(brokerAddr, mq, timestamp, + boundaryType, timeoutMillis); + } catch (Exception e) { + throw new MQClientException("Invoke Broker[" + brokerAddr + "] exception", e); + } + } + + throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null); + } + + public long maxOffset(MessageQueue mq) throws MQClientException { + String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(this.mQClientFactory.getBrokerNameFromMessageQueue(mq)); + if (null == brokerAddr) { + this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic()); + brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(this.mQClientFactory.getBrokerNameFromMessageQueue(mq)); + } + + if (brokerAddr != null) { + try { + return this.mQClientFactory.getMQClientAPIImpl().getMaxOffset(brokerAddr, mq, timeoutMillis); + } catch (Exception e) { + throw new MQClientException("Invoke Broker[" + brokerAddr + "] exception", e); + } + } + + throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null); + } + + public long minOffset(MessageQueue mq) throws MQClientException { + String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(this.mQClientFactory.getBrokerNameFromMessageQueue(mq)); + if (null == brokerAddr) { + this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic()); + brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(this.mQClientFactory.getBrokerNameFromMessageQueue(mq)); + } + + if (brokerAddr != null) { + try { + return this.mQClientFactory.getMQClientAPIImpl().getMinOffset(brokerAddr, mq, timeoutMillis); + } catch (Exception e) { + throw new MQClientException("Invoke Broker[" + brokerAddr + "] exception", e); + } + } + + throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null); + } + + public long earliestMsgStoreTime(MessageQueue mq) throws MQClientException { + String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(this.mQClientFactory.getBrokerNameFromMessageQueue(mq)); + if (null == brokerAddr) { + this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic()); + brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(this.mQClientFactory.getBrokerNameFromMessageQueue(mq)); + } + + if (brokerAddr != null) { + try { + return this.mQClientFactory.getMQClientAPIImpl().getEarliestMsgStoretime(brokerAddr, mq, timeoutMillis); + } catch (Exception e) { + throw new MQClientException("Invoke Broker[" + brokerAddr + "] exception", e); + } + } + + throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null); + } + + public MessageExt viewMessage(String topic, String msgId) + throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + MessageId messageId; + 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(NetworkUtil.socketAddress2String(messageId.getAddress()), + topic, messageId.getOffset(), timeoutMillis); + } + + public QueryResult queryMessage(String topic, String key, int maxNum, long begin, + long end) throws MQClientException, + InterruptedException { + return queryMessage(null, topic, key, maxNum, begin, end, false); + } + + public QueryResult queryMessageByUniqKey(String topic, String uniqKey, int maxNum, long begin, long end) + throws MQClientException, InterruptedException { + + return queryMessage(null, topic, uniqKey, maxNum, begin, end, true); + } + + public QueryResult queryMessageByUniqKey(String clusterName, String topic, String uniqKey, int maxNum, long begin, + long end) + throws MQClientException, InterruptedException { + + return queryMessage(clusterName, 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); + } + + 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 { + return null; + } + } + + public QueryResult queryMessage(String clusterName, String topic, String key, int maxNum, long begin, long end, + boolean isUniqKey) throws MQClientException, + InterruptedException { + boolean isLmq = MixAll.isLmq(topic); + + String routeTopic = topic; + // if topic is lmq ,then use clusterName as lmq parent topic + // Use clusterName or lmq parent topic to get topic route for lmq or rmq_sys_wheel_timer + if (!StringUtils.isEmpty(topic) && (isLmq || topic.equals(TopicValidator.SYSTEM_TOPIC_PREFIX + "wheel_timer")) + && !StringUtils.isEmpty(clusterName)) { + routeTopic = clusterName; + } + + TopicRouteData topicRouteData = this.mQClientFactory.getAnExistTopicRouteData(routeTopic); + if (null == topicRouteData) { + this.mQClientFactory.updateTopicRouteInfoFromNameServer(routeTopic); + topicRouteData = this.mQClientFactory.getAnExistTopicRouteData(routeTopic); + } + + if (topicRouteData != null) { + List brokerAddrs = new LinkedList<>(); + for (BrokerData brokerData : topicRouteData.getBrokerDatas()) { + if (!isLmq && clusterName != null && !clusterName.isEmpty() + && !clusterName.equals(brokerData.getCluster())) { + continue; + } + String addr = brokerData.selectBrokerAddr(); + if (addr != null) { + brokerAddrs.add(addr); + } + } + + if (!brokerAddrs.isEmpty()) { + final CountDownLatch countDownLatch = new CountDownLatch(brokerAddrs.size()); + final List queryResultList = new LinkedList<>(); + final ReadWriteLock lock = new ReentrantReadWriteLock(false); + + for (String addr : brokerAddrs) { + try { + QueryMessageRequestHeader requestHeader = new QueryMessageRequestHeader(); + if (isLmq) { + requestHeader.setTopic(clusterName); + } else { + requestHeader.setTopic(topic); + } + requestHeader.setKey(key); + requestHeader.setMaxNum(maxNum); + requestHeader.setBeginTimestamp(begin); + requestHeader.setEndTimestamp(end); + + this.mQClientFactory.getMQClientAPIImpl().queryMessage(addr, requestHeader, timeoutMillis * 3, + new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + + @Override + public void operationSucceed(RemotingCommand response) { + try { + 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; + } + 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); + } + + } + + boolean ok = countDownLatch.await(timeoutMillis * 4, TimeUnit.MILLISECONDS); + if (!ok) { + log.warn("queryMessage, maybe some broker failed"); + } + + long indexLastUpdateTimestamp = 0; + List messageList = new LinkedList<>(); + for (QueryResult qr : queryResultList) { + if (qr.getIndexLastUpdateTimestamp() > indexLastUpdateTimestamp) { + indexLastUpdateTimestamp = qr.getIndexLastUpdateTimestamp(); + } + + for (MessageExt msgExt : qr.getMessageList()) { + if (isUniqKey) { + if (msgExt.getMsgId().equals(key)) { + 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); + for (String k : keyArray) { + // both topic and key must be equal at the same time + if (Objects.equals(key, k) && (isLmq || Objects.equals(topic, msgTopic))) { + matched = true; + break; + } + } + + if (matched) { + messageList.add(msgExt); + } else { + log.warn("queryMessage, find message key not matched, maybe hash duplicate {}", msgExt.toString()); + } + } + } + } + } + + //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 { + throw new MQClientException(ResponseCode.NO_MESSAGE, "query message by key finished, but no message."); + } + } + } + + throw new MQClientException(ResponseCode.TOPIC_NOT_EXIST, "The topic[" + topic + "] not matched route info"); + } +} 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 new file mode 100644 index 0000000..c001b33 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/MQClientAPIImpl.java @@ -0,0 +1,3528 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; + +import com.alibaba.fastjson.JSON; +import org.apache.commons.lang3.ArrayUtils; +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.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.CheckRocksdbCqWriteResult; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.ObjectCreator; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.UtilAll; +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; +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.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.sysflag.PullSysFlag; +import org.apache.rocketmq.common.topic.TopicValidator; +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.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; +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.LanguageCode; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; +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.AclInfo; +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.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.CreateTopicListRequestBody; +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.SubscriptionGroupList; +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.body.UserInfo; +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.CheckRocksdbCqWriteProgressRequestHeader; +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.CreateAclRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.CreateTopicListRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.CreateTopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.CreateUserRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.DeleteAclRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.DeleteSubscriptionGroupRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.DeleteTopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.DeleteUserRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.EndTransactionRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ExportRocksDBConfigToJsonRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ExtraInfoUtil; +import org.apache.rocketmq.remoting.protocol.header.GetAclRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetAllProducerInfoRequestHeader; +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.GetUserRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.HeartbeatRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ListAclsRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ListUsersRequestHeader; +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.RecallMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.RecallMessageResponseHeader; +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.UpdateAclRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateGroupForbiddenRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateUserRequestHeader; +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 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; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.apache.rocketmq.remoting.protocol.RemotingSysResponseCode.SUCCESS; + +public class MQClientAPIImpl implements NameServerUpdateCallback, StartAndShutdown { + private final static Logger log = LoggerFactory.getLogger(MQClientAPIImpl.class); + private static boolean sendSmartMsg = + Boolean.parseBoolean(System.getProperty("org.apache.rocketmq.client.sendSmartMsg", "true")); + + static { + System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION)); + } + + private final RemotingClient remotingClient; + private final TopAddressing topAddressing; + private final ClientRemotingProcessor clientRemotingProcessor; + private String nameSrvAddr = null; + private ClientConfig clientConfig; + + public MQClientAPIImpl( + final NettyClientConfig nettyClientConfig, + final ClientRemotingProcessor clientRemotingProcessor, + final RPCHook rpcHook, + final ClientConfig clientConfig + ) { + this(nettyClientConfig, clientRemotingProcessor, rpcHook, clientConfig, null); + } + + public MQClientAPIImpl( + final NettyClientConfig nettyClientConfig, + final ClientRemotingProcessor clientRemotingProcessor, + final RPCHook rpcHook, + final ClientConfig clientConfig, + final ChannelEventListener channelEventListener + ) { + this( + nettyClientConfig, + clientRemotingProcessor, + rpcHook, + clientConfig, + channelEventListener, + null + ); + } + + public MQClientAPIImpl(final NettyClientConfig nettyClientConfig, + final ClientRemotingProcessor clientRemotingProcessor, + RPCHook rpcHook, final ClientConfig clientConfig, + final ChannelEventListener channelEventListener, + final ObjectCreator remotingClientCreator) { + this.clientConfig = clientConfig; + topAddressing = new DefaultTopAddressing(MixAll.getWSAddr(), clientConfig.getUnitName()); + topAddressing.registerChangeCallBack(this); + this.remotingClient = remotingClientCreator != null + ? remotingClientCreator.create(nettyClientConfig, channelEventListener) + : 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); + + this.remotingClient.registerProcessor(RequestCode.RESET_CONSUMER_CLIENT_OFFSET, this.clientRemotingProcessor, null); + + this.remotingClient.registerProcessor(RequestCode.GET_CONSUMER_STATUS_FROM_CLIENT, this.clientRemotingProcessor, null); + + 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() { + return this.remotingClient.getNameServerAddressList(); + } + + public RemotingClient getRemotingClient() { + return remotingClient; + } + + public String fetchNameServerAddr() { + try { + String addrs = this.topAddressing.fetchNSAddr(); + if (!UtilAll.isBlank(addrs)) { + if (!addrs.equals(this.nameSrvAddr)) { + log.info("name server address changed, old=" + this.nameSrvAddr + ", new=" + addrs); + this.updateNameServerAddressList(addrs); + this.nameSrvAddr = addrs; + return nameSrvAddr; + } + } + } catch (Exception e) { + log.error("fetchNameServerAddr Exception", e); + } + return nameSrvAddr; + } + + @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; + } + + public void updateNameServerAddressList(final String addrs) { + String[] addrArray = addrs.split(";"); + List list = Arrays.asList(addrArray); + this.remotingClient.updateNameServerAddressList(list); + } + + public void start() { + this.remotingClient.start(); + } + + 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, InterruptedException, MQClientException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_AND_CREATE_SUBSCRIPTIONGROUP, null); + + byte[] body = RemotingSerializable.encode(config); + request.setBody(body); + + 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 createSubscriptionGroupList(final String address, final List configs, + final long timeoutMillis) throws RemotingException, InterruptedException, MQClientException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_AND_CREATE_SUBSCRIPTIONGROUP_LIST, null); + SubscriptionGroupList requestBody = new SubscriptionGroupList(configs); + request.setBody(requestBody.encode()); + + RemotingCommand response = this.remotingClient.invokeSync( + MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), address), request, timeoutMillis); + assert response != null; + if (response.getCode() == ResponseCode.SUCCESS) { + return; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + 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); + 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.setAttributes(AttributeParser.parseToString(topicConfig.getAttributes())); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_AND_CREATE_TOPIC, 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 createTopicList(final String address, final List topicConfigList, final long timeoutMillis) + throws InterruptedException, RemotingException, MQClientException { + CreateTopicListRequestHeader requestHeader = new CreateTopicListRequestHeader(); + CreateTopicListRequestBody requestBody = new CreateTopicListRequestBody(topicConfigList); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_AND_CREATE_TOPIC_LIST, requestHeader); + request.setBody(requestBody.encode()); + + RemotingCommand response = this.remotingClient.invokeSync( + MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), address), request, timeoutMillis); + assert response != null; + if (response.getCode() == ResponseCode.SUCCESS) { + return; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + public SendResult sendMessage( + final String addr, + final String brokerName, + final Message msg, + final SendMessageRequestHeader requestHeader, + final long timeoutMillis, + final CommunicationMode communicationMode, + final SendMessageContext context, + final DefaultMQProducerImpl producer + ) throws RemotingException, MQBrokerException, InterruptedException { + return sendMessage(addr, brokerName, msg, requestHeader, timeoutMillis, communicationMode, null, null, null, 0, context, producer); + } + + public SendResult sendMessage( + final String addr, + final String brokerName, + final Message msg, + final SendMessageRequestHeader requestHeader, + final long timeoutMillis, + final CommunicationMode communicationMode, + final SendCallback sendCallback, + final TopicPublishInfo topicPublishInfo, + final MQClientInstance instance, + final int retryTimesWhenSendFailed, + final SendMessageContext context, + final DefaultMQProducerImpl producer + ) throws RemotingException, MQBrokerException, InterruptedException { + long beginStartTime = System.currentTimeMillis(); + RemotingCommand request = null; + 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 { + 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) { + case ONEWAY: + this.remotingClient.invokeOneway(addr, request, timeoutMillis); + return null; + case ASYNC: + final AtomicInteger times = new AtomicInteger(); + 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: + 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; + } + + return null; + } + + private SendResult sendMessageSync( + final String addr, + final String brokerName, + final Message msg, + final long timeoutMillis, + final RemotingCommand request + ) throws RemotingException, MQBrokerException, InterruptedException { + RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis); + assert response != null; + 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( + final String addr, + final String brokerName, + final Message msg, + final long timeoutMillis, + final RemotingCommand request, + final SendCallback sendCallback, + final TopicPublishInfo topicPublishInfo, + final MQClientInstance instance, + final int retryTimesWhenSendFailed, + final AtomicInteger times, + final SendMessageContext context, + final DefaultMQProducerImpl producer + ) { + final long beginStartTime = System.currentTimeMillis(); + try { + this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + + @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) { + } + + producer.updateFaultItem(brokerName, System.currentTimeMillis() - beginStartTime, false, true); + return; + } + + try { + SendResult sendResult = MQClientAPIImpl.this.processSendResponse(brokerName, msg, response, addr); + assert sendResult != null; + if (context != null) { + context.setSendResult(sendResult); + context.getProducer().executeSendMessageHookAfter(context); + } + + try { + sendCallback.onSuccess(sendResult); + } catch (Throwable e) { + } + + producer.updateFaultItem(brokerName, System.currentTimeMillis() - beginStartTime, false, true); + } catch (Exception e) { + producer.updateFaultItem(brokerName, System.currentTimeMillis() - beginStartTime, true, true); + onExceptionImpl(brokerName, msg, timeoutMillis - cost, request, sendCallback, topicPublishInfo, instance, + retryTimesWhenSendFailed, times, e, context, false, producer); + } + } + + @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 (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("unknown reason", throwable); + boolean needRetry = !(throwable instanceof RemotingTooMuchRequestException); + onExceptionImpl(brokerName, msg, timeoutMillis - cost, request, sendCallback, topicPublishInfo, instance, + retryTimesWhenSendFailed, times, ex, context, needRetry, 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, + final Message msg, + final long timeoutMillis, + final RemotingCommand request, + final SendCallback sendCallback, + final TopicPublishInfo topicPublishInfo, + final MQClientInstance instance, + final int timesTotal, + final AtomicInteger curTimes, + final Exception e, + final SendMessageContext context, + final boolean needRetry, + final DefaultMQProducerImpl producer + ) { + int tmp = curTimes.incrementAndGet(); + if (needRetry && tmp <= timesTotal && timeoutMillis > 0) { + 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, false); + retryBrokerName = instance.getBrokerNameFromMessageQueue(mqChosen); + } + String addr = instance.findBrokerAddressInPublish(retryBrokerName); + 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) { + context.setException(e); + context.getProducer().executeSendMessageHookAfter(context); + } + + try { + sendCallback.onException(e); + } catch (Exception ignored) { + } + } + } + + protected SendResult processSendResponse( + final String brokerName, + final Message msg, + final RemotingCommand response, + final String addr + ) throws MQBrokerException, RemotingCommandException { + SendStatus sendStatus; + 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: { + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + } + + SendMessageResponseHeader responseHeader = + (SendMessageResponseHeader) response.decodeCommandCustomHeader(SendMessageResponseHeader.class); + + //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()); + } + + 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)); + } + uniqMsgId = sb.toString(); + } + SendResult sendResult = new SendResult(sendStatus, + uniqMsgId, + responseHeader.getMsgId(), messageQueue, responseHeader.getQueueOffset()); + sendResult.setTransactionId(responseHeader.getTransactionId()); + sendResult.setRecallHandle(responseHeader.getRecallHandle()); + 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( + final String addr, + final PullMessageRequestHeader requestHeader, + final long timeoutMillis, + final CommunicationMode communicationMode, + final PullCallback pullCallback + ) throws RemotingException, MQBrokerException, InterruptedException { + 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: + assert false; + return null; + case ASYNC: + this.pullMessageAsync(addr, request, timeoutMillis, pullCallback); + return null; + case SYNC: + return this.pullMessageSync(addr, request, timeoutMillis); + default: + assert false; + break; + } + + 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, + final long timeoutMillis, + final PullCallback pullCallback + ) throws RemotingException, InterruptedException { + this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + + @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); + } + }); + } + + private PullResult pullMessageSync( + final String addr, + final RemotingCommand request, + final long timeoutMillis + ) throws RemotingException, InterruptedException, MQBrokerException { + RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis); + assert response != null; + return this.processPullResponse(response, addr); + } + + private PullResult 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 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 + String[] queues = messageExt.getProperty(MessageConst.PROPERTY_INNER_MULTI_DISPATCH) + .split(MixAll.LMQ_DISPATCH_SEPARATOR); + String[] queueOffsets = messageExt.getProperty(MessageConst.PROPERTY_INNER_MULTI_QUEUE_OFFSET) + .split(MixAll.LMQ_DISPATCH_SEPARATOR); + long offset = Long.parseLong(queueOffsets[ArrayUtils.indexOf(queues, topic)]); + // 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, offset); + index = sortMap.get(queueIdKey).indexOf(offset); + msgQueueOffset = msgOffsetInfo.get(queueIdKey).get(index); + if (msgQueueOffset != offset) { + 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(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[{}] of msg is strange, not equal to the stored in msg, {}", 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 + String[] queues = messageExt.getProperty(MessageConst.PROPERTY_INNER_MULTI_DISPATCH) + .split(MixAll.LMQ_DISPATCH_SEPARATOR); + String[] queueOffsets = messageExt.getProperty(MessageConst.PROPERTY_INNER_MULTI_QUEUE_OFFSET) + .split(MixAll.LMQ_DISPATCH_SEPARATOR); + // LMQ topic has only 1 queue, which queue id is 0 + key = ExtraInfoUtil.getStartOffsetInfoMapKey(topic, MixAll.LMQ_QUEUE_ID); + sortMap.putIfAbsent(key, new ArrayList<>(4)); + sortMap.get(key).add(Long.parseLong(queueOffsets[ArrayUtils.indexOf(queues, topic)])); + 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 String topic, final long phyoffset, final long timeoutMillis) + throws RemotingException, MQBrokerException, InterruptedException { + ViewMessageRequestHeader requestHeader = new ViewMessageRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setOffset(phyoffset); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.VIEW_MESSAGE_BY_ID, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + 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(), addr); + } + + @Deprecated + public long searchOffset(final String addr, final String topic, final int queueId, final long timestamp, + final long timeoutMillis) + throws RemotingException, MQBrokerException, InterruptedException { + SearchOffsetRequestHeader requestHeader = new SearchOffsetRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setQueueId(queueId); + requestHeader.setTimestamp(timestamp); + 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 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(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), + request, timeoutMillis); + 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(), addr); + } + + public List getConsumerIdListByGroup( + final String addr, + final String consumerGroup, + final long timeoutMillis) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, + MQBrokerException, InterruptedException { + GetConsumerListByGroupRequestHeader requestHeader = new GetConsumerListByGroupRequestHeader(); + requestHeader.setConsumerGroup(consumerGroup); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_CONSUMER_LIST_BY_GROUP, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + if (response.getBody() != null) { + GetConsumerListByGroupResponseBody body = + GetConsumerListByGroupResponseBody.decode(response.getBody(), GetConsumerListByGroupResponseBody.class); + return body.getConsumerIdList(); + } + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + + public long getMinOffset(final String addr, final MessageQueue messageQueue, final long timeoutMillis) + throws RemotingException, MQBrokerException, InterruptedException { + GetMinOffsetRequestHeader requestHeader = new GetMinOffsetRequestHeader(); + 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), + request, timeoutMillis); + 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(), addr); + } + + public long getEarliestMsgStoretime(final String addr, final MessageQueue mq, final long timeoutMillis) + throws RemotingException, MQBrokerException, InterruptedException { + GetEarliestMsgStoretimeRequestHeader requestHeader = new GetEarliestMsgStoretimeRequestHeader(); + 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), + request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + GetEarliestMsgStoretimeResponseHeader responseHeader = + (GetEarliestMsgStoretimeResponseHeader) response.decodeCommandCustomHeader(GetEarliestMsgStoretimeResponseHeader.class); + + return responseHeader.getTimestamp(); + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + + public long queryConsumerOffset( + final String addr, + final QueryConsumerOffsetRequestHeader requestHeader, + final long timeoutMillis + ) throws RemotingException, MQBrokerException, InterruptedException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_CONSUMER_OFFSET, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + 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(), addr); + } + + public void updateConsumerOffset( + final String addr, + final UpdateConsumerOffsetRequestHeader requestHeader, + final long timeoutMillis + ) throws RemotingException, MQBrokerException, InterruptedException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_CONSUMER_OFFSET, 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 MQBrokerException(response.getCode(), response.getRemark(), addr); + } + + public void updateConsumerOffsetOneway( + final String addr, + final UpdateConsumerOffsetRequestHeader requestHeader, + final long timeoutMillis + ) throws RemotingConnectException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException, + InterruptedException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_CONSUMER_OFFSET, requestHeader); + + this.remotingClient.invokeOneway(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), request, timeoutMillis); + } + + public int sendHeartbeat( + 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: { + return response.getVersion(); + } + default: + 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()); + } + + public void unregisterClient( + final String addr, + final String clientID, + final String producerGroup, + final String consumerGroup, + final long timeoutMillis + ) throws RemotingException, MQBrokerException, InterruptedException { + final UnregisterClientRequestHeader requestHeader = new UnregisterClientRequestHeader(); + requestHeader.setClientID(clientID); + requestHeader.setProducerGroup(producerGroup); + requestHeader.setConsumerGroup(consumerGroup); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UNREGISTER_CLIENT, 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(), addr); + } + + public void endTransactionOneway( + final String addr, + final EndTransactionRequestHeader requestHeader, + final String remark, + final long timeoutMillis + ) throws RemotingException, InterruptedException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.END_TRANSACTION, requestHeader); + + request.setRemark(remark); + this.remotingClient.invokeOneway(addr, request, timeoutMillis); + } + + public void queryMessage( + final String addr, + final QueryMessageRequestHeader requestHeader, + final long timeoutMillis, + final InvokeCallback invokeCallback, + final Boolean isUniqueKey + ) throws RemotingException, MQBrokerException, InterruptedException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_MESSAGE, requestHeader); + request.addExtField(MixAll.UNIQUE_MSG_QUERY_FLAG, isUniqueKey.toString()); + this.remotingClient.invokeAsync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), request, timeoutMillis, + invokeCallback); + } + + public boolean registerClient(final String addr, final HeartbeatData heartbeat, final long timeoutMillis) + throws RemotingException, InterruptedException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.HEART_BEAT, new HeartbeatRequestHeader()); + + request.setBody(heartbeat.encode()); + RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis); + return response.getCode() == ResponseCode.SUCCESS; + } + + public void consumerSendMessageBack( + final String addr, + final String brokerName, + final MessageExt msg, + final String consumerGroup, + final int delayLevel, + final long timeoutMillis, + final int maxConsumeRetryTimes + ) throws RemotingException, MQBrokerException, InterruptedException { + ConsumerSendMsgBackRequestHeader requestHeader = new ConsumerSendMsgBackRequestHeader(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CONSUMER_SEND_MSG_BACK, requestHeader); + + requestHeader.setGroup(consumerGroup); + requestHeader.setOriginTopic(msg.getTopic()); + requestHeader.setOffset(msg.getCommitLogOffset()); + 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); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return; + } + default: + break; + } + + 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, new LockBatchMqRequestHeader()); + + request.setBody(requestBody.encode()); + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + LockBatchResponseBody responseBody = LockBatchResponseBody.decode(response.getBody(), LockBatchResponseBody.class); + Set messageQueues = responseBody.getLockOKMQSet(); + return messageQueues; + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + + public void unlockBatchMQ( + final String addr, + final UnlockBatchRequestBody requestBody, + final long timeoutMillis, + final boolean oneway + ) throws RemotingException, MQBrokerException, InterruptedException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UNLOCK_BATCH_MQ, new UnlockBatchMqRequestHeader()); + + request.setBody(requestBody.encode()); + + if (oneway) { + this.remotingClient.invokeOneway(addr, request, timeoutMillis); + } else { + 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(), addr); + } + } + + public TopicStatsTable getTopicStatsInfo(final String addr, final String topic, + final long timeoutMillis) throws InterruptedException, + RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQBrokerException { + GetTopicStatsInfoRequestHeader requestHeader = new GetTopicStatsInfoRequestHeader(); + requestHeader.setTopic(topic); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_TOPIC_STATS_INFO, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + TopicStatsTable topicStatsTable = TopicStatsTable.decode(response.getBody(), TopicStatsTable.class); + return topicStatsTable; + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + + public ConsumeStats getConsumeStats(final String addr, final String consumerGroup, final long timeoutMillis) + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, + MQBrokerException { + return getConsumeStats(addr, consumerGroup, null, null, timeoutMillis); + } + + public ConsumeStats getConsumeStats(final String addr, final String consumerGroup, final List topicList, + final long timeoutMillis) throws RemotingSendRequestException, RemotingConnectException, RemotingTimeoutException, MQBrokerException, InterruptedException { + return getConsumeStats(addr, consumerGroup, null, topicList, timeoutMillis); + } + + public ConsumeStats getConsumeStats(final String addr, final String consumerGroup, final String topic, + final long timeoutMillis) throws RemotingSendRequestException, RemotingConnectException, RemotingTimeoutException, MQBrokerException, InterruptedException { + return getConsumeStats(addr, consumerGroup, topic, null, timeoutMillis); + } + + public ConsumeStats getConsumeStats(final String addr, final String consumerGroup, final String topic, + final List topicList, final long timeoutMillis) + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, + MQBrokerException { + GetConsumeStatsRequestHeader requestHeader = new GetConsumeStatsRequestHeader(); + requestHeader.setConsumerGroup(consumerGroup); + requestHeader.setTopic(topic); + requestHeader.updateTopicList(topicList); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_CONSUME_STATS, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + ConsumeStats consumeStats = ConsumeStats.decode(response.getBody(), ConsumeStats.class); + return consumeStats; + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + + public ProducerConnection getProducerConnectionList(final String addr, final String producerGroup, + final long timeoutMillis) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, + MQBrokerException { + GetProducerConnectionListRequestHeader requestHeader = new GetProducerConnectionListRequestHeader(); + requestHeader.setProducerGroup(producerGroup); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_PRODUCER_CONNECTION_LIST, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return ProducerConnection.decode(response.getBody(), ProducerConnection.class); + } + default: + break; + } + + 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, + final long timeoutMillis) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, + MQBrokerException { + GetConsumerConnectionListRequestHeader requestHeader = new GetConsumerConnectionListRequestHeader(); + requestHeader.setConsumerGroup(consumerGroup); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_CONSUMER_CONNECTION_LIST, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return ConsumerConnection.decode(response.getBody(), ConsumerConnection.class); + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + + public KVTable getBrokerRuntimeInfo(final String addr, final long timeoutMillis) throws RemotingConnectException, + RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException { + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_BROKER_RUNTIME_INFO, null); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return KVTable.decode(response.getBody(), KVTable.class); + } + default: + 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, MQClientException, UnsupportedEncodingException { + Validators.checkBrokerConfig(properties); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_BROKER_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(), addr); + } + } + + public Properties getBrokerConfig(final String addr, final long timeoutMillis) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, + MQBrokerException, UnsupportedEncodingException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_BROKER_CONFIG, null); + + RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return MixAll.string2Properties(new String(response.getBody(), MixAll.DEFAULT_CHARSET)); + } + default: + 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()); + } + + public ClusterInfo getBrokerClusterInfo( + final long timeoutMillis) throws InterruptedException, RemotingTimeoutException, + RemotingSendRequestException, RemotingConnectException, MQBrokerException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_BROKER_CLUSTER_INFO, null); + + RemotingCommand response = this.remotingClient.invokeSync(null, request, timeoutMillis); + 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 TopicRouteData getDefaultTopicRouteInfoFromNameServer(final long timeoutMillis) + throws RemotingException, MQClientException, InterruptedException { + + 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); + } + + public TopicRouteData getTopicRouteInfoFromNameServer(final String topic, final long timeoutMillis, + boolean allowTopicNotExist) throws MQClientException, 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) { + log.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 MQClientException(response.getCode(), response.getRemark()); + } + + 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()) { + case ResponseCode.SUCCESS: { + byte[] body = response.getBody(); + if (body != null) { + return TopicList.decode(body, TopicList.class); + } + } + default: + break; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + public int wipeWritePermOfBroker(final String namesrvAddr, String brokerName, + final long timeoutMillis) throws RemotingCommandException, + RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQClientException { + WipeWritePermOfBrokerRequestHeader requestHeader = new WipeWritePermOfBrokerRequestHeader(); + 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()) { + case ResponseCode.SUCCESS: { + WipeWritePermOfBrokerResponseHeader responseHeader = + (WipeWritePermOfBrokerResponseHeader) response.decodeCommandCustomHeader(WipeWritePermOfBrokerResponseHeader.class); + return responseHeader.getWipeTopicCount(); + } + default: + break; + } + + 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, 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; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return; + } + default: + break; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + public void deleteTopicInNameServer(final String addr, final String topic, final long timeoutMillis) + 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()) { + case ResponseCode.SUCCESS: { + return; + } + default: + break; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + 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), + request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return; + } + default: + break; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + public String getKVConfigValue(final String namespace, final String key, final long timeoutMillis) + throws RemotingException, MQClientException, InterruptedException { + GetKVConfigRequestHeader requestHeader = new GetKVConfigRequestHeader(); + requestHeader.setNamespace(namespace); + 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()) { + case ResponseCode.SUCCESS: { + GetKVConfigResponseHeader responseHeader = + (GetKVConfigResponseHeader) response.decodeCommandCustomHeader(GetKVConfigResponseHeader.class); + return responseHeader.getValue(); + } + default: + break; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + public void putKVConfigValue(final String namespace, final String key, final String value, final long timeoutMillis) + throws RemotingException, MQClientException, InterruptedException { + PutKVConfigRequestHeader requestHeader = new PutKVConfigRequestHeader(); + requestHeader.setNamespace(namespace); + requestHeader.setKey(key); + requestHeader.setValue(value); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PUT_KV_CONFIG, requestHeader); + + List nameServerAddressList = this.remotingClient.getNameServerAddressList(); + if (nameServerAddressList != null) { + RemotingCommand errResponse = null; + for (String namesrvAddr : nameServerAddressList) { + RemotingCommand response = this.remotingClient.invokeSync(namesrvAddr, 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 void deleteKVConfigValue(final String namespace, final String key, final long timeoutMillis) + throws RemotingException, MQClientException, InterruptedException { + DeleteKVConfigRequestHeader requestHeader = new DeleteKVConfigRequestHeader(); + requestHeader.setNamespace(namespace); + requestHeader.setKey(key); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.DELETE_KV_CONFIG, requestHeader); + + List nameServerAddressList = this.remotingClient.getNameServerAddressList(); + if (nameServerAddressList != null) { + RemotingCommand errResponse = null; + for (String namesrvAddr : nameServerAddressList) { + RemotingCommand response = this.remotingClient.invokeSync(namesrvAddr, 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 KVTable getKVListByNamespace(final String namespace, final long timeoutMillis) + throws RemotingException, MQClientException, InterruptedException { + GetKVListByNamespaceRequestHeader requestHeader = new GetKVListByNamespaceRequestHeader(); + 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()) { + case ResponseCode.SUCCESS: { + return KVTable.decode(response.getBody(), KVTable.class); + } + 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) + throws RemotingException, MQClientException, InterruptedException { + 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 { + ResetOffsetRequestHeader requestHeader = new ResetOffsetRequestHeader(); + requestHeader.setTopic(topic); + 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; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + if (response.getBody() != null) { + ResetOffsetBody body = ResetOffsetBody.decode(response.getBody(), ResetOffsetBody.class); + return body.getOffsetTable(); + } + } + default: + break; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + public Map> invokeBrokerToGetConsumerStatus(final String addr, final String topic, + final String group, + final String clientAddr, + final long timeoutMillis) throws RemotingException, MQClientException, InterruptedException { + GetConsumerStatusRequestHeader requestHeader = new GetConsumerStatusRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setGroup(group); + 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; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + if (response.getBody() != null) { + GetConsumerStatusBody body = GetConsumerStatusBody.decode(response.getBody(), GetConsumerStatusBody.class); + return body.getConsumerTable(); + } + } + default: + break; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + public GroupList queryTopicConsumeByWho(final String addr, final String topic, final long timeoutMillis) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, + MQBrokerException { + QueryTopicConsumeByWhoRequestHeader requestHeader = new QueryTopicConsumeByWhoRequestHeader(); + 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()) { + case ResponseCode.SUCCESS: { + GroupList groupList = GroupList.decode(response.getBody(), GroupList.class); + return groupList; + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + + public TopicList queryTopicsByConsumer(final String addr, final String group, final long timeoutMillis) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, + MQBrokerException { + QueryTopicsByConsumerRequestHeader requestHeader = new QueryTopicsByConsumerRequestHeader(); + requestHeader.setGroup(group); + + 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: { + TopicList topicList = TopicList.decode(response.getBody(), TopicList.class); + return topicList; + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + 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 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: { + QuerySubscriptionResponseBody subscriptionResponseBody = + QuerySubscriptionResponseBody.decode(response.getBody(), QuerySubscriptionResponseBody.class); + return subscriptionResponseBody.getSubscriptionData(); + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + 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.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; + } + + 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: { + byte[] body = response.getBody(); + if (body != null) { + TopicList topicList = TopicList.decode(body, TopicList.class); + return topicList; + } + } + default: + break; + } + + 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()) { + case ResponseCode.SUCCESS: { + byte[] body = response.getBody(); + if (body != null) { + TopicList topicList = TopicList.decode(response.getBody(), TopicList.class); + if (topicList.getTopicList() != null && !topicList.getTopicList().isEmpty() + && !UtilAll.isBlank(topicList.getBrokerAddr())) { + TopicList tmp = getSystemTopicListFromBroker(topicList.getBrokerAddr(), timeoutMillis); + if (tmp.getTopicList() != null && !tmp.getTopicList().isEmpty()) { + topicList.getTopicList().addAll(tmp.getTopicList()); + } + } + return topicList; + } + } + default: + break; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + 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; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + byte[] body = response.getBody(); + if (body != null) { + TopicList topicList = TopicList.decode(body, TopicList.class); + return topicList; + } + } + default: + break; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + public boolean cleanExpiredConsumeQueue(final String addr, + long timeoutMillis) throws MQClientException, RemotingConnectException, + RemotingSendRequestException, RemotingTimeoutException, InterruptedException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CLEAN_EXPIRED_CONSUMEQUEUE, 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 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 { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CLEAN_UNUSED_TOPIC, 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 ConsumerRunningInfo getConsumerRunningInfo(final String addr, String consumerGroup, String clientId, + boolean jstack, + final long timeoutMillis) throws RemotingException, MQClientException, InterruptedException { + GetConsumerRunningInfoRequestHeader requestHeader = new GetConsumerRunningInfoRequestHeader(); + requestHeader.setConsumerGroup(consumerGroup); + requestHeader.setClientId(clientId); + requestHeader.setJstackEnable(jstack); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_CONSUMER_RUNNING_INFO, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + byte[] body = response.getBody(); + if (body != null) { + ConsumerRunningInfo info = ConsumerRunningInfo.decode(body, ConsumerRunningInfo.class); + return info; + } + } + default: + break; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + 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); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CONSUME_MESSAGE_DIRECTLY, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + byte[] body = response.getBody(); + if (body != null) { + ConsumeMessageDirectlyResult info = ConsumeMessageDirectlyResult.decode(body, ConsumeMessageDirectlyResult.class); + return info; + } + } + default: + break; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + public Map queryCorrectionOffset(final String addr, final String topic, final String group, + Set filterGroup, + long timeoutMillis) throws MQClientException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, + InterruptedException { + QueryCorrectionOffsetHeader requestHeader = new QueryCorrectionOffsetHeader(); + requestHeader.setCompareGroup(group); + requestHeader.setTopic(topic); + if (filterGroup != null) { + StringBuilder sb = new StringBuilder(); + String splitor = ""; + for (String s : filterGroup) { + sb.append(splitor).append(s); + splitor = ","; + } + 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; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + if (response.getBody() != null) { + QueryCorrectionOffsetBody body = QueryCorrectionOffsetBody.decode(response.getBody(), QueryCorrectionOffsetBody.class); + return body.getCorrectionOffsets(); + } + } + default: + break; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + 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()) { + case ResponseCode.SUCCESS: { + byte[] body = response.getBody(); + if (body != null) { + TopicList topicList = TopicList.decode(response.getBody(), TopicList.class); + if (!containRetry) { + Iterator it = topicList.getTopicList().iterator(); + while (it.hasNext()) { + String topic = it.next(); + if (topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + it.remove(); + } + } + } + + return topicList; + } + } + default: + break; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + 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()) { + case ResponseCode.SUCCESS: { + byte[] body = response.getBody(); + if (body != null) { + TopicList topicList = TopicList.decode(response.getBody(), TopicList.class); + if (!containRetry) { + Iterator it = topicList.getTopicList().iterator(); + while (it.hasNext()) { + String topic = it.next(); + if (topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + it.remove(); + } + } + } + return topicList; + } + } + default: + break; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + 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()) { + case ResponseCode.SUCCESS: { + byte[] body = response.getBody(); + if (body != null) { + TopicList topicList = TopicList.decode(response.getBody(), TopicList.class); + if (!containRetry) { + Iterator it = topicList.getTopicList().iterator(); + while (it.hasNext()) { + String topic = it.next(); + if (topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + it.remove(); + } + } + } + return topicList; + } + } + default: + break; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + public void cloneGroupOffset(final String addr, final String srcGroup, final String destGroup, final String topic, + final boolean isOffline, + final long timeoutMillis) throws RemotingException, MQClientException, InterruptedException { + CloneGroupOffsetRequestHeader requestHeader = new CloneGroupOffsetRequestHeader(); + requestHeader.setSrcGroup(srcGroup); + requestHeader.setDestGroup(destGroup); + 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; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return; + } + default: + break; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + public BrokerStatsData viewBrokerStatsData(String brokerAddr, String statsName, String statsKey, long timeoutMillis) + throws MQClientException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, + InterruptedException { + ViewBrokerStatsDataRequestHeader requestHeader = new ViewBrokerStatsDataRequestHeader(); + requestHeader.setStatsName(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; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + byte[] body = response.getBody(); + if (body != null) { + return BrokerStatsData.decode(body, BrokerStatsData.class); + } + } + default: + break; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + public Set getClusterList(String topic, + long timeoutMillis) { + return Collections.EMPTY_SET; + } + + public ConsumeStatsList fetchConsumeStatsInBroker(String brokerAddr, boolean isOrder, + long timeoutMillis) throws MQClientException, + RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException { + GetConsumeStatsInBrokerHeader requestHeader = new GetConsumeStatsInBrokerHeader(); + 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; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + byte[] body = response.getBody(); + if (body != null) { + return ConsumeStatsList.decode(body, ConsumeStatsList.class); + } + } + default: + break; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + public SubscriptionGroupWrapper getAllSubscriptionGroup(final String brokerAddr, + long timeoutMillis) throws InterruptedException, + RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQBrokerException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ALL_SUBSCRIPTIONGROUP_CONFIG, null); + RemotingCommand response = this.remotingClient + .invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), brokerAddr), request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return SubscriptionGroupWrapper.decode(response.getBody(), SubscriptionGroupWrapper.class); + } + default: + break; + } + 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, + long timeoutMillis) throws RemotingConnectException, + RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ALL_TOPIC_CONFIG, null); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return TopicConfigSerializeWrapper.decode(response.getBody(), TopicConfigSerializeWrapper.class); + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + + public void updateNameServerConfig(final Properties properties, final List nameServers, long timeoutMillis) + throws UnsupportedEncodingException, InterruptedException, RemotingTimeoutException, RemotingSendRequestException, + RemotingConnectException, MQClientException { + String str = MixAll.properties2String(properties); + if (str == null || str.length() < 1) { + return; + } + List invokeNameServers = (nameServers == null || nameServers.isEmpty()) ? + this.remotingClient.getNameServerAddressList() : nameServers; + if (invokeNameServers == null || invokeNameServers.isEmpty()) { + return; + } + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_NAMESRV_CONFIG, null); + request.setBody(str.getBytes(MixAll.DEFAULT_CHARSET)); + + RemotingCommand errResponse = null; + for (String nameServer : invokeNameServers) { + RemotingCommand response = this.remotingClient.invokeSync(nameServer, 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 Map getNameServerConfig(final List nameServers, long timeoutMillis) + throws InterruptedException, + RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, + MQClientException, UnsupportedEncodingException { + List invokeNameServers = (nameServers == null || nameServers.isEmpty()) ? + this.remotingClient.getNameServerAddressList() : nameServers; + if (invokeNameServers == null || invokeNameServers.isEmpty()) { + return null; + } + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_NAMESRV_CONFIG, null); + + Map configMap = new HashMap<>(4); + for (String nameServer : invokeNameServers) { + RemotingCommand response = this.remotingClient.invokeSync(nameServer, request, timeoutMillis); + + assert response != null; + + if (ResponseCode.SUCCESS == response.getCode()) { + configMap.put(nameServer, MixAll.string2Properties(new String(response.getBody(), MixAll.DEFAULT_CHARSET))); + } else { + throw new MQClientException(response.getCode(), response.getRemark()); + } + } + return configMap; + } + + public QueryConsumeQueueResponseBody queryConsumeQueue(final String brokerAddr, final String topic, + final int queueId, + final long index, final int count, final String consumerGroup, + final long timeoutMillis) throws InterruptedException, + RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQClientException { + + QueryConsumeQueueRequestHeader requestHeader = new QueryConsumeQueueRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setQueueId(queueId); + requestHeader.setIndex(index); + requestHeader.setCount(count); + 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; + + if (ResponseCode.SUCCESS == response.getCode()) { + return QueryConsumeQueueResponseBody.decode(response.getBody(), QueryConsumeQueueResponseBody.class); + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + public CheckRocksdbCqWriteResult checkRocksdbCqWriteProgress(final String brokerAddr, final String topic, final long checkStoreTime, final long timeoutMillis) throws InterruptedException, + RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQClientException { + CheckRocksdbCqWriteProgressRequestHeader header = new CheckRocksdbCqWriteProgressRequestHeader(); + header.setTopic(topic); + header.setCheckStoreTime(checkStoreTime); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CHECK_ROCKSDB_CQ_WRITE_PROGRESS, header); + RemotingCommand response = this.remotingClient.invokeSync(brokerAddr, request, timeoutMillis); + assert response != null; + if (ResponseCode.SUCCESS == response.getCode()) { + return JSON.parseObject(response.getBody(), CheckRocksdbCqWriteResult.class); + } + throw new MQClientException(response.getCode(), response.getRemark()); + } + + public void exportRocksDBConfigToJson(final String brokerAddr, + final List configType, + final long timeoutMillis) throws InterruptedException, + RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQClientException { + ExportRocksDBConfigToJsonRequestHeader header = new ExportRocksDBConfigToJsonRequestHeader(); + header.updateConfigType(configType); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.EXPORT_ROCKSDB_CONFIG_TO_JSON, header); + RemotingCommand response = this.remotingClient.invokeSync(brokerAddr, request, timeoutMillis); + assert response != null; + + if (ResponseCode.SUCCESS != response.getCode()) { + throw new MQClientException(response.getCode(), response.getRemark()); + } + } + + public void checkClientInBroker(final String brokerAddr, final String consumerGroup, + final String clientId, final SubscriptionData subscriptionData, + final long timeoutMillis) + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, + RemotingConnectException, MQClientException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CHECK_CLIENT_CONFIG, null); + + CheckClientRequestBody requestBody = new CheckClientRequestBody(); + requestBody.setClientId(clientId); + requestBody.setGroup(consumerGroup); + requestBody.setSubscriptionData(subscriptionData); + + 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 boolean resumeCheckHalfMessage(final String addr, String topic, String msgId, + final long timeoutMillis) throws RemotingException, InterruptedException { + ResumeCheckHalfMessageRequestHeader requestHeader = new ResumeCheckHalfMessageRequestHeader(); + requestHeader.setTopic(topic); + 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()); + } + + public void createUser(String addr, UserInfo userInfo, long millis) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException { + CreateUserRequestHeader requestHeader = new CreateUserRequestHeader(userInfo.getUsername()); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.AUTH_CREATE_USER, requestHeader); + request.setBody(RemotingSerializable.encode(userInfo)); + RemotingCommand response = this.remotingClient.invokeSync(addr, request, millis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return; + } + default: + break; + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public void updateUser(String addr, UserInfo userInfo, long millis) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException { + UpdateUserRequestHeader requestHeader = new UpdateUserRequestHeader(userInfo.getUsername()); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.AUTH_UPDATE_USER, requestHeader); + request.setBody(RemotingSerializable.encode(userInfo)); + RemotingCommand response = this.remotingClient.invokeSync(addr, request, millis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return; + } + default: + break; + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public void deleteUser(String addr, String username, long millis) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException { + DeleteUserRequestHeader requestHeader = new DeleteUserRequestHeader(username); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.AUTH_DELETE_USER, requestHeader); + RemotingCommand response = this.remotingClient.invokeSync(addr, request, millis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return; + } + default: + break; + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public UserInfo getUser(String addr, String username, long millis) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException { + GetUserRequestHeader requestHeader = new GetUserRequestHeader(username); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.AUTH_GET_USER, requestHeader); + RemotingCommand response = this.remotingClient.invokeSync(addr, request, millis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return RemotingSerializable.decode(response.getBody(), UserInfo.class); + } + default: + break; + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public List listUser(String addr, String filter, long millis) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException { + ListUsersRequestHeader requestHeader = new ListUsersRequestHeader(filter); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.AUTH_LIST_USER, requestHeader); + RemotingCommand response = this.remotingClient.invokeSync(addr, request, millis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return RemotingSerializable.decodeList(response.getBody(), UserInfo.class); + } + default: + break; + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public void createAcl(String addr, AclInfo aclInfo, long millis) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException { + CreateAclRequestHeader requestHeader = new CreateAclRequestHeader(aclInfo.getSubject()); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.AUTH_CREATE_ACL, requestHeader); + request.setBody(RemotingSerializable.encode(aclInfo)); + RemotingCommand response = this.remotingClient.invokeSync(addr, request, millis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return; + } + default: + break; + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public void updateAcl(String addr, AclInfo aclInfo, long millis) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException { + UpdateAclRequestHeader requestHeader = new UpdateAclRequestHeader(aclInfo.getSubject()); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.AUTH_UPDATE_ACL, requestHeader); + request.setBody(RemotingSerializable.encode(aclInfo)); + RemotingCommand response = this.remotingClient.invokeSync(addr, request, millis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return; + } + default: + break; + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public void deleteAcl(String addr, String subject, String resource, long millis) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException { + DeleteAclRequestHeader requestHeader = new DeleteAclRequestHeader(subject, resource); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.AUTH_DELETE_ACL, requestHeader); + RemotingCommand response = this.remotingClient.invokeSync(addr, request, millis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return; + } + default: + break; + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public AclInfo getAcl(String addr, String subject, long millis) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException { + GetAclRequestHeader requestHeader = new GetAclRequestHeader(subject); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.AUTH_GET_ACL, requestHeader); + RemotingCommand response = this.remotingClient.invokeSync(addr, request, millis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return RemotingSerializable.decode(response.getBody(), AclInfo.class); + } + default: + break; + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public List listAcl(String addr, String subjectFilter, String resourceFilter, long millis) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException { + ListAclsRequestHeader requestHeader = new ListAclsRequestHeader(subjectFilter, resourceFilter); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.AUTH_LIST_ACL, requestHeader); + RemotingCommand response = this.remotingClient.invokeSync(addr, request, millis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return RemotingSerializable.decodeList(response.getBody(), AclInfo.class); + } + default: + break; + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public String recallMessage( + final String addr, + RecallMessageRequestHeader requestHeader, + final long timeoutMillis + ) throws RemotingException, MQBrokerException, InterruptedException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.RECALL_MESSAGE, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis); + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + RecallMessageResponseHeader responseHeader = + response.decodeCommandCustomHeader(RecallMessageResponseHeader.class); + return responseHeader.getMsgId(); + } + default: + break; + } + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + + public void recallMessageAsync( + final String addr, + final RecallMessageRequestHeader requestHeader, + final long timeoutMillis, + final InvokeCallback invokeCallback + ) throws RemotingException, InterruptedException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.RECALL_MESSAGE, requestHeader); + + this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + } + + @Override + public void operationSucceed(RemotingCommand response) { + invokeCallback.operationSucceed(response); + } + + @Override + public void operationFail(Throwable throwable) { + invokeCallback.operationFail(throwable); + } + }); + } + + public void exportPopRecord(String brokerAddr, long timeout) throws RemotingConnectException, + RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException { + RemotingCommand request = RemotingCommand.createRequestCommand( + RequestCode.POP_ROLLBACK, null); + RemotingCommand response = this.remotingClient.invokeSync(brokerAddr, request, timeout); + assert response != null; + if (response.getCode() == SUCCESS) { + return; + } + 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 new file mode 100644 index 0000000..ca6f461 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/MQClientManager.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.client.impl; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +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.producer.ProduceAccumulator; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class MQClientManager { + 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<>(); + private ConcurrentMap accumulatorTable = + new ConcurrentHashMap(); + + + private MQClientManager() { + + } + + public static MQClientManager getInstance() { + return instance; + } + + public MQClientInstance getOrCreateMQClientInstance(final ClientConfig clientConfig) { + return getOrCreateMQClientInstance(clientConfig, null); + } + public MQClientInstance getOrCreateMQClientInstance(final ClientConfig clientConfig, RPCHook rpcHook) { + String clientId = clientConfig.buildMQClientId(); + MQClientInstance instance = this.factoryTable.get(clientId); + if (null == instance) { + instance = + new MQClientInstance(clientConfig.cloneClientConfig(), + this.factoryIndexGenerator.getAndIncrement(), clientId, rpcHook); + MQClientInstance prev = this.factoryTable.putIfAbsent(clientId, instance); + if (prev != null) { + instance = prev; + log.warn("Returned Previous MQClientInstance for clientId:[{}]", clientId); + } else { + log.info("Created new MQClientInstance for clientId:[{}]", clientId); + } + } + + 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); + } + + public ConcurrentMap getFactoryTable() { + return factoryTable; + } +} 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 0000000..34f066c --- /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 0000000..a57cb53 --- /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 new file mode 100644 index 0000000..b151fef --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageConcurrentlyService.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.client.impl.consumer; + +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.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.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.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.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class ConsumeMessageConcurrentlyService implements ConsumeMessageService { + private static final Logger log = LoggerFactory.getLogger(ConsumeMessageConcurrentlyService.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; + private final ScheduledExecutorService cleanExpireMsgExecutors; + + public ConsumeMessageConcurrentlyService(DefaultMQPushConsumerImpl defaultMQPushConsumerImpl, + MessageListenerConcurrently messageListener) { + this.defaultMQPushConsumerImpl = defaultMQPushConsumerImpl; + this.messageListener = messageListener; + + this.defaultMQPushConsumer = this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer(); + this.consumerGroup = this.defaultMQPushConsumer.getConsumerGroup(); + 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_" + consumerGroupTag)); + + this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("ConsumeMessageScheduledThread_" + consumerGroupTag)); + this.cleanExpireMsgExecutors = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("CleanExpireMsgScheduledThread_" + consumerGroupTag)); + } + + public void start() { + this.cleanExpireMsgExecutors.scheduleAtFixedRate(new Runnable() { + + @Override + public void run() { + try { + cleanExpireMsg(); + } catch (Throwable e) { + log.error("scheduleAtFixedRate cleanExpireMsg exception", e); + } + } + + }, this.defaultMQPushConsumer.getConsumeTimeout(), this.defaultMQPushConsumer.getConsumeTimeout(), TimeUnit.MINUTES); + } + + public void shutdown(long awaitTerminateMillis) { + this.scheduledExecutorService.shutdown(); + ThreadUtils.shutdownGracefully(this.consumeExecutor, awaitTerminateMillis, TimeUnit.MILLISECONDS); + this.cleanExpireMsgExecutors.shutdown(); + } + + @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); + + msg.setBrokerName(brokerName); + 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("consumeMessageDirectly exception: {} Group: {} Msgs: {} MQ: {}", + UtilAll.exceptionSimpleDesc(e), + ConsumeMessageConcurrentlyService.this.consumerGroup, + msgs, + mq, e); + } + + result.setSpentTimeMills(System.currentTimeMillis() - beginTime); + + log.info("consumeMessageDirectly Result: {}", result); + + return result; + } + + @Override + public void submitConsumeRequest( + final List msgs, + final ProcessQueue processQueue, + final MessageQueue messageQueue, + final boolean dispatchToConsume) { + 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); + } + } + } + } + + @Override + public void submitPopConsumeRequest(final List msgs, + final PopProcessQueue processQueue, + final MessageQueue messageQueue) { + throw new UnsupportedOperationException(); + } + + private void cleanExpireMsg() { + Iterator> it = + this.defaultMQPushConsumerImpl.getRebalanceImpl().getProcessQueueTable().entrySet().iterator(); + while (it.hasNext()) { + Map.Entry next = it.next(); + ProcessQueue pq = next.getValue(); + pq.cleanExpiredMsg(this.defaultMQPushConsumer); + } + } + + public void processConsumeResult( + final ConsumeConcurrentlyStatus status, + final ConsumeConcurrentlyContext context, + final ConsumeRequest consumeRequest + ) { + int ackIndex = context.getAckIndex(); + + if (consumeRequest.getMsgs().isEmpty()) + return; + + 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, consumeRequest.getMessageQueue().getTopic(), ok); + this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), failed); + break; + case RECONSUME_LATER: + ackIndex = -1; + this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), + consumeRequest.getMsgs().size()); + break; + default: + break; + } + + switch (this.defaultMQPushConsumer.getMessageModel()) { + case BROADCASTING: + for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) { + MessageExt msg = consumeRequest.getMsgs().get(i); + log.warn("BROADCASTING, the message consume failed, drop it, {}", msg.toString()); + } + break; + case CLUSTERING: + 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); + msgBackFailed.add(msg); + } + } + + if (!msgBackFailed.isEmpty()) { + consumeRequest.getMsgs().removeAll(msgBackFailed); + + this.submitConsumeRequestLater(msgBackFailed, consumeRequest.getProcessQueue(), consumeRequest.getMessageQueue()); + } + break; + default: + break; + } + + long offset = consumeRequest.getProcessQueue().removeMessage(consumeRequest.getMsgs()); + if (offset >= 0 && !consumeRequest.getProcessQueue().isDropped()) { + this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(consumeRequest.getMessageQueue(), offset, true); + } + } + + public ConsumerStatsManager getConsumerStatsManager() { + return this.defaultMQPushConsumerImpl.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, this.defaultMQPushConsumer.queueWithNamespace(context.getMessageQueue())); + return true; + } catch (Exception e) { + log.error("sendMessageBack exception, group: " + this.consumerGroup + " msg: " + msg, e); + } + + return false; + } + + private void submitConsumeRequestLater( + final List msgs, + final ProcessQueue processQueue, + final MessageQueue messageQueue + ) { + + this.scheduledExecutorService.schedule(new Runnable() { + + @Override + public void run() { + ConsumeMessageConcurrentlyService.this.submitConsumeRequest(msgs, processQueue, messageQueue, true); + } + }, 5000, TimeUnit.MILLISECONDS); + } + + private void submitConsumeRequestLater(final ConsumeRequest consumeRequest + ) { + + this.scheduledExecutorService.schedule(new Runnable() { + + @Override + public void run() { + ConsumeMessageConcurrentlyService.this.consumeExecutor.submit(consumeRequest); + } + }, 5000, TimeUnit.MILLISECONDS); + } + + class ConsumeRequest implements Runnable { + private final List msgs; + private final ProcessQueue processQueue; + private final MessageQueue messageQueue; + + public ConsumeRequest(List msgs, ProcessQueue processQueue, MessageQueue messageQueue) { + this.msgs = msgs; + this.processQueue = processQueue; + this.messageQueue = messageQueue; + } + + public List getMsgs() { + return msgs; + } + + public ProcessQueue getProcessQueue() { + return processQueue; + } + + @Override + public void run() { + if (this.processQueue.isDropped()) { + log.info("the message queue not be able to consume, because it's dropped. group={} {}", ConsumeMessageConcurrentlyService.this.consumerGroup, this.messageQueue); + return; + } + + 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.setMq(messageQueue); + consumeMessageContext.setMsgList(msgs); + consumeMessageContext.setSuccess(false); + ConsumeMessageConcurrentlyService.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), + ConsumeMessageConcurrentlyService.this.consumerGroup, + msgs, + messageQueue, e); + hasException = true; + } + long consumeRT = System.currentTimeMillis() - beginTimestamp; + if (null == status) { + if (hasException) { + returnType = ConsumeReturnType.EXCEPTION; + } else { + returnType = ConsumeReturnType.RETURNNULL; + } + } else if (consumeRT >= defaultMQPushConsumer.getConsumeTimeout() * 60 * 1000) { + returnType = ConsumeReturnType.TIME_OUT; + } else if (ConsumeConcurrentlyStatus.RECONSUME_LATER == status) { + returnType = ConsumeReturnType.FAILED; + } else if (ConsumeConcurrentlyStatus.CONSUME_SUCCESS == status) { + returnType = ConsumeReturnType.SUCCESS; + } + + if (ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.hasHook()) { + consumeMessageContext.getProps().put(MixAll.CONSUME_CONTEXT_TYPE, returnType.name()); + } + + if (null == status) { + log.warn("consumeMessage return null, Group: {} Msgs: {} MQ: {}", + ConsumeMessageConcurrentlyService.this.consumerGroup, + msgs, + messageQueue); + status = ConsumeConcurrentlyStatus.RECONSUME_LATER; + } + + if (ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.hasHook()) { + consumeMessageContext.setStatus(status.toString()); + consumeMessageContext.setSuccess(ConsumeConcurrentlyStatus.CONSUME_SUCCESS == status); + consumeMessageContext.setAccessChannel(defaultMQPushConsumer.getAccessChannel()); + ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.executeHookAfter(consumeMessageContext); + } + + ConsumeMessageConcurrentlyService.this.getConsumerStatsManager() + .incConsumeRT(ConsumeMessageConcurrentlyService.this.consumerGroup, messageQueue.getTopic(), consumeRT); + + if (!processQueue.isDropped()) { + ConsumeMessageConcurrentlyService.this.processConsumeResult(status, context, this); + } else { + log.warn("processQueue is dropped without process consume result. messageQueue={}, msgs={}", messageQueue, msgs); + } + } + + public MessageQueue getMessageQueue() { + return messageQueue; + } + + } +} 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 new file mode 100644 index 0000000..3ca465d --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageOrderlyService.java @@ -0,0 +1,573 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.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 ConsumeMessageOrderlyService implements ConsumeMessageService { + 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; + private final DefaultMQPushConsumer defaultMQPushConsumer; + private final MessageListenerOrderly messageListener; + private final BlockingQueue consumeRequestQueue; + private final ThreadPoolExecutor consumeExecutor; + private final String consumerGroup; + private final MessageQueueLock messageQueueLock = new MessageQueueLock(); + private final ScheduledExecutorService scheduledExecutorService; + private volatile boolean stopped = false; + + public ConsumeMessageOrderlyService(DefaultMQPushConsumerImpl defaultMQPushConsumerImpl, + MessageListenerOrderly messageListener) { + this.defaultMQPushConsumerImpl = defaultMQPushConsumerImpl; + this.messageListener = messageListener; + + this.defaultMQPushConsumer = this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer(); + this.consumerGroup = this.defaultMQPushConsumer.getConsumerGroup(); + 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_" + consumerGroupTag)); + + this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("ConsumeMessageScheduledThread_" + consumerGroupTag)); + } + + @Override + public void start() { + if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())) { + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + ConsumeMessageOrderlyService.this.lockMQPeriodically(); + } catch (Throwable e) { + log.error("scheduleAtFixedRate lockMQPeriodically exception", e); + } + } + }, 1000, 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.unlockAllMQ(); + } + } + + public synchronized void unlockAllMQ() { + 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("consumeMessageDirectly exception: {} Group: {} Msgs: {} MQ: {}", + UtilAll.exceptionSimpleDesc(e), + ConsumeMessageOrderlyService.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( + final List msgs, + final ProcessQueue processQueue, + final MessageQueue messageQueue, + final boolean dispatchToConsume) { + if (dispatchToConsume) { + ConsumeRequest consumeRequest = new ConsumeRequest(processQueue, messageQueue); + this.consumeExecutor.submit(consumeRequest); + } + } + + @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(); + } + } + + public void tryLockLaterAndReconsume(final MessageQueue mq, final ProcessQueue processQueue, + final long delayMills) { + this.scheduledExecutorService.schedule(new Runnable() { + @Override + public void run() { + boolean lockOK = ConsumeMessageOrderlyService.this.lockOneMQ(mq); + if (lockOK) { + ConsumeMessageOrderlyService.this.submitConsumeRequestLater(processQueue, mq, 10); + } else { + ConsumeMessageOrderlyService.this.submitConsumeRequestLater(processQueue, mq, 3000); + } + } + }, delayMills, TimeUnit.MILLISECONDS); + } + + public synchronized boolean lockOneMQ(final MessageQueue mq) { + if (!this.stopped) { + return this.defaultMQPushConsumerImpl.getRebalanceImpl().lock(mq); + } + + return false; + } + + private void submitConsumeRequestLater( + final ProcessQueue processQueue, + final MessageQueue messageQueue, + 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() { + ConsumeMessageOrderlyService.this.submitConsumeRequest(null, processQueue, messageQueue, true); + } + }, timeMillis, TimeUnit.MILLISECONDS); + } + + public boolean processConsumeResult( + final List msgs, + final ConsumeOrderlyStatus status, + final ConsumeOrderlyContext context, + final ConsumeRequest consumeRequest + ) { + boolean continueConsume = true; + long commitOffset = -1L; + if (context.isAutoCommit()) { + switch (status) { + case COMMIT: + case ROLLBACK: + log.warn("the message queue consume result is illegal, we think you want to ack these message {}", + consumeRequest.getMessageQueue()); + case SUCCESS: + commitOffset = consumeRequest.getProcessQueue().commit(); + this.getConsumerStatsManager().incConsumeOKTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size()); + break; + case SUSPEND_CURRENT_QUEUE_A_MOMENT: + this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size()); + if (checkReconsumeTimes(msgs)) { + consumeRequest.getProcessQueue().makeMessageToConsumeAgain(msgs); + this.submitConsumeRequestLater( + consumeRequest.getProcessQueue(), + consumeRequest.getMessageQueue(), + context.getSuspendCurrentQueueTimeMillis()); + continueConsume = false; + } else { + commitOffset = consumeRequest.getProcessQueue().commit(); + } + break; + default: + break; + } + } else { + switch (status) { + case SUCCESS: + this.getConsumerStatsManager().incConsumeOKTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size()); + break; + case COMMIT: + commitOffset = consumeRequest.getProcessQueue().commit(); + break; + case ROLLBACK: + consumeRequest.getProcessQueue().rollback(); + this.submitConsumeRequestLater( + consumeRequest.getProcessQueue(), + consumeRequest.getMessageQueue(), + context.getSuspendCurrentQueueTimeMillis()); + continueConsume = false; + break; + case SUSPEND_CURRENT_QUEUE_A_MOMENT: + this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size()); + if (checkReconsumeTimes(msgs)) { + consumeRequest.getProcessQueue().makeMessageToConsumeAgain(msgs); + this.submitConsumeRequestLater( + consumeRequest.getProcessQueue(), + consumeRequest.getMessageQueue(), + context.getSuspendCurrentQueueTimeMillis()); + continueConsume = false; + } + break; + default: + break; + } + } + + if (commitOffset >= 0 && !consumeRequest.getProcessQueue().isDropped()) { + this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(consumeRequest.getMessageQueue(), commitOffset, false); + } + + return continueConsume; + } + + 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() + 1)); + MessageAccessor.setMaxReconsumeTimes(newMsg, String.valueOf(getMaxReconsumeTimes())); + MessageAccessor.clearProperty(newMsg, MessageConst.PROPERTY_TRANSACTION_PREPARED); + 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 ProcessQueue processQueue; + private final MessageQueue messageQueue; + + public ConsumeRequest(ProcessQueue processQueue, MessageQueue messageQueue) { + this.processQueue = processQueue; + this.messageQueue = messageQueue; + } + + public ProcessQueue getProcessQueue() { + return processQueue; + } + + public MessageQueue getMessageQueue() { + return messageQueue; + } + + @Override + public void run() { + if (this.processQueue.isDropped()) { + log.warn("run, the message queue not be able to consume, because it's dropped. {}", this.messageQueue); + return; + } + + final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue); + synchronized (objLock) { + if (MessageModel.BROADCASTING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel()) + || this.processQueue.isLocked() && !this.processQueue.isLockExpired()) { + final long beginTime = System.currentTimeMillis(); + for (boolean continueConsume = true; continueConsume; ) { + if (this.processQueue.isDropped()) { + log.warn("the message queue not be able to consume, because it's dropped. {}", this.messageQueue); + break; + } + + if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel()) + && !this.processQueue.isLocked()) { + log.warn("the message queue not locked, so consume later, {}", this.messageQueue); + ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 10); + break; + } + + if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel()) + && this.processQueue.isLockExpired()) { + log.warn("the message queue lock expired, so consume later, {}", this.messageQueue); + ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 10); + break; + } + + long interval = System.currentTimeMillis() - beginTime; + if (interval > MAX_TIME_CONSUME_CONTINUOUSLY) { + ConsumeMessageOrderlyService.this.submitConsumeRequestLater(processQueue, messageQueue, 10); + break; + } + + final int consumeBatchSize = + ConsumeMessageOrderlyService.this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize(); + + List msgs = this.processQueue.takeMessages(consumeBatchSize); + defaultMQPushConsumerImpl.resetRetryAndNamespace(msgs, defaultMQPushConsumer.getConsumerGroup()); + if (!msgs.isEmpty()) { + final ConsumeOrderlyContext context = new ConsumeOrderlyContext(this.messageQueue); + + ConsumeOrderlyStatus status = null; + + ConsumeMessageContext consumeMessageContext = null; + if (ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.hasHook()) { + 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<>()); + ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.executeHookBefore(consumeMessageContext); + } + + long beginTimestamp = System.currentTimeMillis(); + ConsumeReturnType returnType = ConsumeReturnType.SUCCESS; + boolean hasException = false; + try { + 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); + break; + } + + status = messageListener.consumeMessage(Collections.unmodifiableList(msgs), context); + } catch (Throwable e) { + log.warn("consumeMessage exception: {} Group: {} Msgs: {} MQ: {}", + UtilAll.exceptionSimpleDesc(e), + ConsumeMessageOrderlyService.this.consumerGroup, + msgs, + messageQueue, e); + hasException = true; + } finally { + this.processQueue.getConsumeLock().readLock().unlock(); + } + + if (null == status + || ConsumeOrderlyStatus.ROLLBACK == status + || ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT == status) { + log.warn("consumeMessage Orderly return not OK, Group: {} Msgs: {} MQ: {}", + ConsumeMessageOrderlyService.this.consumerGroup, + msgs, + messageQueue); + } + + long consumeRT = System.currentTimeMillis() - beginTimestamp; + if (null == status) { + if (hasException) { + returnType = ConsumeReturnType.EXCEPTION; + } else { + returnType = ConsumeReturnType.RETURNNULL; + } + } else if (consumeRT >= defaultMQPushConsumer.getConsumeTimeout() * 60 * 1000) { + returnType = ConsumeReturnType.TIME_OUT; + } else if (ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT == status) { + returnType = ConsumeReturnType.FAILED; + } else if (ConsumeOrderlyStatus.SUCCESS == status) { + returnType = ConsumeReturnType.SUCCESS; + } + + if (ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.hasHook()) { + consumeMessageContext.getProps().put(MixAll.CONSUME_CONTEXT_TYPE, returnType.name()); + } + + if (null == status) { + status = ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT; + } + + if (ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.hasHook()) { + consumeMessageContext.setStatus(status.toString()); + consumeMessageContext + .setSuccess(ConsumeOrderlyStatus.SUCCESS == status || ConsumeOrderlyStatus.COMMIT == status); + consumeMessageContext.setAccessChannel(defaultMQPushConsumer.getAccessChannel()); + ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.executeHookAfter(consumeMessageContext); + } + + ConsumeMessageOrderlyService.this.getConsumerStatsManager() + .incConsumeRT(ConsumeMessageOrderlyService.this.consumerGroup, messageQueue.getTopic(), consumeRT); + + continueConsume = ConsumeMessageOrderlyService.this.processConsumeResult(msgs, status, context, this); + } else { + continueConsume = false; + } + } + } else { + if (this.processQueue.isDropped()) { + log.warn("the message queue not be able to consume, because it's dropped. {}", this.messageQueue); + return; + } + + ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 100); + } + } + } + + } + +} 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 0000000..d519187 --- /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("consumeMessageDirectly exception: {} Group: {} Msgs: {} MQ: {}", + 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 or popTimeout. 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 0000000..4eab1cc --- /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("consumeMessageDirectly exception: {} Group: {} Msgs: {} MQ: {}", + 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 new file mode 100644 index 0000000..ee68473 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageService.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.client.impl.consumer; + +import java.util.List; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; + +public interface ConsumeMessageService { + void start(); + + void shutdown(long awaitTerminateMillis); + + void updateCorePoolSize(int corePoolSize); + + void incCorePoolSize(); + + void decCorePoolSize(); + + int getCorePoolSize(); + + ConsumeMessageDirectlyResult consumeMessageDirectly(final MessageExt msg, final String brokerName); + + void submitConsumeRequest( + final List msgs, + 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 0000000..f85dcc7 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/DefaultLitePullConsumerImpl.java @@ -0,0 +1,1320 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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(); + } + + initScheduledThreadPoolExecutor(); + + initMQClientFactory(); + + initRebalanceImpl(); + + initPullAPIWrapper(); + + initOffsetStore(); + + mQClientFactory.start(); + + startScheduleTask(); + + this.serviceState = ServiceState.RUNNING; + + log.info("the consumer [{}] start OK", this.defaultLitePullConsumer.getConsumerGroup()); + + try { + operateAfterRunning(); + } catch (Exception e) { + shutdown(); + throw e; + } + + 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 initScheduledThreadPoolExecutor() { + this.scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor( + this.defaultLitePullConsumer.getPullThreadNums(), + new ThreadFactoryImpl("PullMsgThread-" + this.defaultLitePullConsumer.getConsumerGroup()) + ); + } + + 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={}", + (int)Math.ceil((double)defaultLitePullConsumer.getPullThresholdForAll() / defaultLitePullConsumer.getPullBatchSize()), + 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. + String namespace = this.defaultLitePullConsumer.getNamespace(); + if (namespace != null) { + for (MessageExt messageExt : msgList) { + messageExt.setTopic(NamespaceUtil.withoutNamespace(messageExt.getTopic(), namespace)); + } + } + } + + 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()) { + return false; + } + + for (MessageQueue messageQueue : set2) { + if (!set1.contains(messageQueue)) { + return false; + } + } + return true; + } + + 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 new file mode 100644 index 0000000..9d46e28 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/DefaultMQPullConsumerImpl.java @@ -0,0 +1,882 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +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; +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.UtilAll; +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.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.sysflag.PullSysFlag; +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.common.RemotingHelper; +import org.apache.rocketmq.remoting.exception.RemotingException; +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; + +/** + * 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 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 volatile ServiceState serviceState = ServiceState.CREATE_JUST; + protected MQClientInstance mQClientFactory; + private PullAPIWrapper pullAPIWrapper; + private OffsetStore offsetStore; + private RebalanceImpl rebalanceImpl = new RebalancePullImpl(this); + + public DefaultMQPullConsumerImpl(final DefaultMQPullConsumer defaultMQPullConsumer, final RPCHook rpcHook) { + this.defaultMQPullConsumer = defaultMQPullConsumer; + this.rpcHook = rpcHook; + } + + public void registerConsumeMessageHook(final ConsumeMessageHook hook) { + this.consumeMessageHookList.add(hook); + log.info("register consumeMessageHook Hook, {}", hook.hookName()); + } + + 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, int topicSysFlag) throws MQClientException { + this.isRunning(); + this.mQClientFactory.getMQAdminImpl().createTopic(key, newTopic, queueNum, topicSysFlag, null); + } + + private void isRunning() throws MQClientException { + if (this.serviceState != ServiceState.RUNNING) { + throw new MQClientException("The consumer is not in running status, " + + this.serviceState + + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK), + null); + } + } + + public long fetchConsumeOffset(MessageQueue mq, boolean fromStore) throws MQClientException { + 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.isRunning(); + if (null == topic) { + throw new IllegalArgumentException("topic is null"); + } + + ConcurrentMap mqTable = this.rebalanceImpl.getProcessQueueTable(); + Set mqResult = new HashSet<>(); + for (MessageQueue mq : mqTable.keySet()) { + if (mq.getTopic().equals(topic)) { + mqResult.add(mq); + } + } + + return parseSubscribeMessageQueues(mqResult); + } + + public List fetchPublishMessageQueues(String topic) throws MQClientException { + this.isRunning(); + return this.mQClientFactory.getMQAdminImpl().fetchPublishMessageQueues(topic); + } + + public Set fetchSubscribeMessageQueues(String topic) throws MQClientException { + 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.isRunning(); + return this.mQClientFactory.getMQAdminImpl().earliestMsgStoreTime(mq); + } + + public long maxOffset(MessageQueue mq) throws MQClientException { + this.isRunning(); + return this.mQClientFactory.getMQAdminImpl().maxOffset(mq); + } + + public long minOffset(MessageQueue mq) throws MQClientException { + this.isRunning(); + return this.mQClientFactory.getMQAdminImpl().minOffset(mq); + } + + public PullResult pull(MessageQueue mq, String subExpression, long offset, int maxNums) + throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + return pull(mq, subExpression, offset, maxNums, this.defaultMQPullConsumer.getConsumerPullTimeoutMillis()); + } + + public PullResult pull(MessageQueue mq, String subExpression, long offset, int maxNums, long timeout) + throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + SubscriptionData subscriptionData = getSubscriptionData(mq, subExpression); + return this.pullSyncImpl(mq, subscriptionData, offset, maxNums, false, timeout); + } + + public PullResult pull(MessageQueue mq, MessageSelector messageSelector, long offset, int maxNums) + throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + 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) { + throw new MQClientException("offset < 0", null); + } + + if (maxNums <= 0) { + throw new MQClientException("maxNums <= 0", null); + } + + this.subscriptionAutomatically(mq.getTopic()); + + int sysFlag = PullSysFlag.buildSysFlag(false, block, true, false); + + long timeoutMillis = block ? this.defaultMQPullConsumer.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.defaultMQPullConsumer.getBrokerSuspendMaxTimeMillis(), + timeoutMillis, + CommunicationMode.SYNC, + 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()); + consumeMessageContext.setSuccess(false); + 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. + String namespace = this.getDefaultMQPullConsumer().getNamespace(); + if (namespace != null) { + for (MessageExt messageExt : msgList) { + messageExt.setTopic(NamespaceUtil.withoutNamespace(messageExt.getTopic(), namespace)); + } + } + } + + public void subscriptionAutomatically(final String topic) { + if (!this.rebalanceImpl.getSubscriptionInner().containsKey(topic)) { + try { + SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(topic, SubscriptionData.SUB_ALL); + this.rebalanceImpl.subscriptionInner.putIfAbsent(topic, subscriptionData); + } catch (Exception ignore) { + } + } + } + + public void unsubscribe(String topic) { + this.rebalanceImpl.getSubscriptionInner().remove(topic); + } + + @Override + public String groupName() { + return this.defaultMQPullConsumer.getConsumerGroup(); + } + + public void executeHookBefore(final ConsumeMessageContext context) { + if (!this.consumeMessageHookList.isEmpty()) { + for (ConsumeMessageHook hook : this.consumeMessageHookList) { + try { + hook.consumeMessageBefore(context); + } catch (Throwable ignored) { + } + } + } + } + + public void executeHookAfter(final ConsumeMessageContext context) { + if (!this.consumeMessageHookList.isEmpty()) { + for (ConsumeMessageHook hook : this.consumeMessageHookList) { + try { + hook.consumeMessageAfter(context); + } catch (Throwable ignored) { + } + } + } + } + + @Override + public MessageModel messageModel() { + return this.defaultMQPullConsumer.getMessageModel(); + } + + @Override + public ConsumeType consumeType() { + return ConsumeType.CONSUME_ACTIVELY; + } + + @Override + public ConsumeFromWhere consumeFromWhere() { + return ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET; + } + + @Override + public Set subscriptions() { + Set registerSubscriptions = defaultMQPullConsumer.getRegisterSubscriptions(); + if (registerSubscriptions != null && !registerSubscriptions.isEmpty()) { + return registerSubscriptions; + } + + Set result = new HashSet<>(); + + Set topics = this.defaultMQPullConsumer.getRegisterTopics(); + if (topics != null) { + synchronized (topics) { + for (String t : topics) { + SubscriptionData ms = null; + try { + ms = FilterAPI.buildSubscriptionData(t, SubscriptionData.SUB_ALL); + } catch (Exception e) { + log.error("parse subscription error", e); + } + if (ms != null) { + ms.setSubVersion(0L); + result.add(ms); + } + } + } + } + + return result; + } + + @Override + public void doRebalance() { + if (!defaultMQPullConsumer.isEnableRebalance()) { + return; + } + if (this.rebalanceImpl != null) { + this.rebalanceImpl.doRebalance(false); + } + } + + @Override + public boolean tryRebalance() { + if (!defaultMQPullConsumer.isEnableRebalance()) { + return true; + } + + if (this.rebalanceImpl != null) { + return this.rebalanceImpl.doRebalance(false); + } + return false; + } + + @Override + public void persistConsumerOffset() { + try { + this.isRunning(); + Set mqs = new HashSet<>(); + Set allocateMq = this.rebalanceImpl.getProcessQueueTable().keySet(); + mqs.addAll(allocateMq); + this.offsetStore.persistAll(mqs); + } catch (Exception e) { + log.error("group: " + this.defaultMQPullConsumer.getConsumerGroup() + " persistConsumerOffset exception", 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.defaultMQPullConsumer.isUnitMode(); + } + + @Override + public ConsumerRunningInfo consumerRunningInfo() { + ConsumerRunningInfo info = new ConsumerRunningInfo(); + + Properties prop = MixAll.object2Properties(this.defaultMQPullConsumer); + prop.put(ConsumerRunningInfo.PROP_CONSUMER_START_TIMESTAMP, String.valueOf(this.consumerStartTimestamp)); + info.setProperties(prop); + + info.getSubscriptionSet().addAll(this.subscriptions()); + return info; + } + + public void pull(MessageQueue mq, String subExpression, long offset, int maxNums, PullCallback pullCallback) + throws MQClientException, RemotingException, InterruptedException { + pull(mq, subExpression, offset, maxNums, pullCallback, this.defaultMQPullConsumer.getConsumerPullTimeoutMillis()); + } + + public void pull(MessageQueue mq, String subExpression, long offset, int maxNums, PullCallback pullCallback, + long timeout) + throws MQClientException, RemotingException, InterruptedException { + 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 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.isRunning(); + + 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); + } + + if (maxSizeInBytes <= 0) { + throw new MQClientException("maxSizeInBytes <= 0", null); + } + + + if (null == pullCallback) { + throw new MQClientException("pullCallback is null", null); + } + + this.subscriptionAutomatically(mq.getTopic()); + + try { + int sysFlag = PullSysFlag.buildSysFlag(false, block, true, false); + + long timeoutMillis = block ? this.defaultMQPullConsumer.getConsumerTimeoutMillisWhenSuspend() : timeout; + + boolean isTagType = ExpressionType.isTagType(subscriptionData.getExpressionType()); + this.pullAPIWrapper.pullKernelImpl( + mq, + subscriptionData.getSubString(), + subscriptionData.getExpressionType(), + isTagType ? 0L : subscriptionData.getSubVersion(), + offset, + maxNums, + maxSizeInBytes, + sysFlag, + 0, + this.defaultMQPullConsumer.getBrokerSuspendMaxTimeMillis(), + timeoutMillis, + CommunicationMode.ASYNC, + new PullCallback() { + + @Override + public void onSuccess(PullResult pullResult) { + PullResult userPullResult = DefaultMQPullConsumerImpl.this.pullAPIWrapper.processPullResult(mq, pullResult, subscriptionData); + resetTopic(userPullResult.getMsgFoundList()); + pullCallback.onSuccess(userPullResult); + } + + @Override + public void onException(Throwable e) { + pullCallback.onException(e); + } + }); + } catch (MQBrokerException e) { + throw new MQClientException("pullAsync unknow exception", 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 { + SubscriptionData subscriptionData = getSubscriptionData(mq, subExpression); + return this.pullSyncImpl(mq, subscriptionData, offset, maxNums, true, this.getDefaultMQPullConsumer().getConsumerPullTimeoutMillis()); + } + + public DefaultMQPullConsumer getDefaultMQPullConsumer() { + return defaultMQPullConsumer; + } + + public void pullBlockIfNotFound(MessageQueue mq, String subExpression, long offset, int maxNums, + PullCallback pullCallback) + throws MQClientException, RemotingException, InterruptedException { + SubscriptionData subscriptionData = getSubscriptionData(mq, subExpression); + this.pullAsyncImpl(mq, subscriptionData, offset, maxNums, pullCallback, true, + this.getDefaultMQPullConsumer().getConsumerPullTimeoutMillis()); + } + + public void pullBlockIfNotFoundWithMessageSelector(MessageQueue mq, MessageSelector messageSelector, long offset, int maxNums, + PullCallback pullCallback) + throws MQClientException, RemotingException, InterruptedException { + SubscriptionData subscriptionData = getSubscriptionData(mq, messageSelector); + this.pullAsyncImpl(mq, subscriptionData, offset, maxNums, pullCallback, true, + this.getDefaultMQPullConsumer().getConsumerPullTimeoutMillis()); + } + + public PullResult pullBlockIfNotFoundWithMessageSelector(MessageQueue mq, MessageSelector messageSelector, long offset, int maxNums) + throws MQClientException, RemotingException, InterruptedException, MQBrokerException { + SubscriptionData subscriptionData = getSubscriptionData(mq, messageSelector); + return this.pullSyncImpl(mq, subscriptionData, offset, maxNums, true, this.getDefaultMQPullConsumer().getConsumerPullTimeoutMillis()); + } + + + public QueryResult queryMessage(String topic, String key, int maxNum, long begin, long end) + throws MQClientException, InterruptedException { + this.isRunning(); + return this.mQClientFactory.getMQAdminImpl().queryMessage(topic, key, maxNum, begin, end); + } + + public MessageExt queryMessageByUniqKey(String topic, String uniqKey) + throws MQClientException, InterruptedException { + this.isRunning(); + return this.mQClientFactory.getMQAdminImpl().queryMessageByUniqKey(topic, uniqKey); + } + + public long searchOffset(MessageQueue mq, long timestamp) throws MQClientException { + this.isRunning(); + return this.mQClientFactory.getMQAdminImpl().searchOffset(mq, timestamp); + } + + public void sendMessageBack(MessageExt msg, int delayLevel, final String brokerName) + throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + sendMessageBack(msg, delayLevel, brokerName, this.defaultMQPullConsumer.getConsumerGroup()); + } + + public void updateConsumeOffsetToBroker(MessageQueue mq, long offset, boolean isOneway) throws RemotingException, + MQBrokerException, InterruptedException, MQClientException { + 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 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(brokerAddr)) { + throw new MQClientException("Broker[" + destBrokerName + "] master node does not exist", null); + } + + if (UtilAll.isBlank(consumerGroup)) { + consumerGroup = this.defaultMQPullConsumer.getConsumerGroup(); + } + + this.mQClientFactory.getMQClientAPIImpl().consumerSendMessageBack(brokerAddr, brokerName, msg, consumerGroup, + delayLevel, 3000, this.defaultMQPullConsumer.getMaxReconsumeTimes()); + } catch (Exception e) { + log.error("sendMessageBack Exception, " + this.defaultMQPullConsumer.getConsumerGroup(), e); + + Message newMsg = new Message(MixAll.getRetryTopic(this.defaultMQPullConsumer.getConsumerGroup()), msg.getBody()); + 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() + 1)); + 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())); + } + } + + public synchronized void shutdown() { + switch (this.serviceState) { + case CREATE_JUST: + break; + case RUNNING: + this.persistConsumerOffset(); + this.mQClientFactory.unregisterConsumer(this.defaultMQPullConsumer.getConsumerGroup()); + this.mQClientFactory.shutdown(); + log.info("the consumer [{}] shutdown OK", this.defaultMQPullConsumer.getConsumerGroup()); + this.serviceState = ServiceState.SHUTDOWN_ALREADY; + break; + case SHUTDOWN_ALREADY: + break; + default: + break; + } + } + + public synchronized void start() throws MQClientException { + switch (this.serviceState) { + case CREATE_JUST: + this.serviceState = ServiceState.START_FAILED; + + this.checkConfig(); + + this.copySubscription(); + + if (this.defaultMQPullConsumer.getMessageModel() == MessageModel.CLUSTERING) { + this.defaultMQPullConsumer.changeInstanceNameToPID(); + } + + this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPullConsumer, this.rpcHook); + + this.rebalanceImpl.setConsumerGroup(this.defaultMQPullConsumer.getConsumerGroup()); + this.rebalanceImpl.setMessageModel(this.defaultMQPullConsumer.getMessageModel()); + this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPullConsumer.getAllocateMessageQueueStrategy()); + this.rebalanceImpl.setmQClientFactory(this.mQClientFactory); + + this.pullAPIWrapper = new PullAPIWrapper( + mQClientFactory, + this.defaultMQPullConsumer.getConsumerGroup(), isUnitMode()); + this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList); + + if (this.defaultMQPullConsumer.getOffsetStore() != null) { + this.offsetStore = this.defaultMQPullConsumer.getOffsetStore(); + } else { + switch (this.defaultMQPullConsumer.getMessageModel()) { + case BROADCASTING: + this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPullConsumer.getConsumerGroup()); + break; + case CLUSTERING: + this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPullConsumer.getConsumerGroup()); + break; + default: + break; + } + this.defaultMQPullConsumer.setOffsetStore(this.offsetStore); + } + + this.offsetStore.load(); + + boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPullConsumer.getConsumerGroup(), this); + if (!registerOK) { + this.serviceState = ServiceState.CREATE_JUST; + + throw new MQClientException("The consumer group[" + this.defaultMQPullConsumer.getConsumerGroup() + + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL), + null); + } + + mQClientFactory.start(); + log.info("the consumer [{}] start OK", this.defaultMQPullConsumer.getConsumerGroup()); + this.serviceState = ServiceState.RUNNING; + 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 checkConfig() throws MQClientException { + // check consumerGroup + Validators.checkGroup(this.defaultMQPullConsumer.getConsumerGroup()); + + // consumerGroup + if (null == this.defaultMQPullConsumer.getConsumerGroup()) { + throw new MQClientException( + "consumerGroup is null" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + + // consumerGroup + if (this.defaultMQPullConsumer.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); + } + + // messageModel + if (null == this.defaultMQPullConsumer.getMessageModel()) { + throw new MQClientException( + "messageModel is null" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + + // allocateMessageQueueStrategy + if (null == this.defaultMQPullConsumer.getAllocateMessageQueueStrategy()) { + throw new MQClientException( + "allocateMessageQueueStrategy is null" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + + // allocateMessageQueueStrategy + if (this.defaultMQPullConsumer.getConsumerTimeoutMillisWhenSuspend() < this.defaultMQPullConsumer.getBrokerSuspendMaxTimeMillis()) { + throw new MQClientException( + "Long polling mode, the consumer consumerTimeoutMillisWhenSuspend must greater than brokerSuspendMaxTimeMillis" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + } + + private void copySubscription() throws MQClientException { + try { + Set registerTopics = this.defaultMQPullConsumer.getRegisterTopics(); + if (registerTopics != null) { + for (final String topic : registerTopics) { + SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(topic, SubscriptionData.SUB_ALL); + this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData); + } + } + } catch (Exception e) { + throw new MQClientException("subscription exception", e); + } + } + + public void updateConsumeOffset(MessageQueue mq, long offset) throws MQClientException { + this.isRunning(); + this.offsetStore.updateOffset(mq, offset, false); + } + + public MessageExt viewMessage(String topic, String msgId) + throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + this.isRunning(); + return this.mQClientFactory.getMQAdminImpl().viewMessage(topic, msgId); + } + + public void registerFilterMessageHook(final FilterMessageHook hook) { + this.filterMessageHookList.add(hook); + log.info("register FilterMessageHook Hook, {}", hook.hookName()); + } + + public OffsetStore getOffsetStore() { + return offsetStore; + } + + public void setOffsetStore(OffsetStore offsetStore) { + this.offsetStore = offsetStore; + } + + public PullAPIWrapper getPullAPIWrapper() { + return pullAPIWrapper; + } + + public void setPullAPIWrapper(PullAPIWrapper pullAPIWrapper) { + this.pullAPIWrapper = pullAPIWrapper; + } + + public ServiceState getServiceState() { + return serviceState; + } + + //Don't use this deprecated setter, which will be removed soon. + @Deprecated + public void setServiceState(ServiceState serviceState) { + this.serviceState = serviceState; + } + + public long getConsumerStartTimestamp() { + return consumerStartTimestamp; + } + + public RebalanceImpl getRebalanceImpl() { + return rebalanceImpl; + } +} 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 new file mode 100644 index 0000000..c93cff4 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/DefaultMQPushConsumerImpl.java @@ -0,0 +1,1601 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +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; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; +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.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.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.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.sysflag.PullSysFlag; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.common.RemotingHelper; +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.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 long pullTimeDelayMillsWhenException = 3000; + /** + * 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_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 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 long consumerStartTimestamp = System.currentTimeMillis(); + private final ArrayList consumeMessageHookList = new ArrayList<>(); + private final RPCHook rpcHook; + private volatile ServiceState serviceState = ServiceState.CREATE_JUST; + private MQClientInstance mQClientFactory; + private PullAPIWrapper pullAPIWrapper; + private volatile boolean pause = false; + private boolean consumeOrderly = false; + 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) { + this.filterMessageHookList.add(hook); + log.info("register FilterMessageHook Hook, {}", hook.hookName()); + } + + public boolean hasHook() { + return !this.consumeMessageHookList.isEmpty(); + } + + 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.warn("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.warn("consumeMessageHook {} executeHookAfter exception", hook.hookName(), e); + } + } + } + } + + 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, int topicSysFlag) throws MQClientException { + this.mQClientFactory.getMQAdminImpl().createTopic(key, newTopic, queueNum, topicSysFlag, null); + } + + public Set fetchSubscribeMessageQueues(String topic) throws MQClientException { + Set result = this.rebalanceImpl.getTopicSubscribeInfoTable().get(topic); + if (null == result) { + this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic); + result = this.rebalanceImpl.getTopicSubscribeInfoTable().get(topic); + } + + if (null == result) { + throw new MQClientException("The topic[" + topic + "] not exist", null); + } + + 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() { + return defaultMQPushConsumer; + } + + public long earliestMsgStoreTime(MessageQueue mq) throws MQClientException { + return this.mQClientFactory.getMQAdminImpl().earliestMsgStoreTime(mq); + } + + public long maxOffset(MessageQueue mq) throws MQClientException { + return this.mQClientFactory.getMQAdminImpl().maxOffset(mq); + } + + public long minOffset(MessageQueue mq) throws MQClientException { + return this.mQClientFactory.getMQAdminImpl().minOffset(mq); + } + + public OffsetStore getOffsetStore() { + return offsetStore; + } + + public void setOffsetStore(OffsetStore offsetStore) { + this.offsetStore = offsetStore; + } + + public void pullMessage(final PullRequest pullRequest) { + final ProcessQueue processQueue = pullRequest.getProcessQueue(); + if (processQueue.isDropped()) { + log.info("the pull request[{}] is dropped.", pullRequest.toString()); + return; + } + + pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis()); + + try { + this.makeSureStateOK(); + } catch (MQClientException e) { + log.warn("pullMessage exception, consumer state not ok", e); + this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException); + return; + } + + if (this.isPause()) { + log.warn("consumer was paused, execute pull request later. instanceName={}, group={}", this.defaultMQPushConsumer.getInstanceName(), this.defaultMQPushConsumer.getConsumerGroup()); + this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_SUSPEND); + return; + } + + long cachedMessageCount = processQueue.getMsgCount().get(); + long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024); + + if (cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue()) { + 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={}", + this.defaultMQPushConsumer.getPullThresholdForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes); + } + return; + } + + if (cachedMessageSizeInMiB > this.defaultMQPushConsumer.getPullThresholdSizeForQueue()) { + 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={}", + this.defaultMQPushConsumer.getPullThresholdSizeForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes); + } + return; + } + + if (!this.consumeOrderly) { + if (processQueue.getMaxSpan() > this.defaultMQPushConsumer.getConsumeConcurrentlyMaxSpan()) { + 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={}", + processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), processQueue.getMaxSpan(), + pullRequest, queueMaxSpanFlowControlTimes); + } + return; + } + } else { + if (processQueue.isLocked()) { + 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); + if (brokerBusy) { + log.info("[NOTIFYME]the first time to pull message, but pull request offset larger than broker consume offset. pullRequest: {} NewOffset: {}", + pullRequest, offset); + } + + pullRequest.setPreviouslyLocked(true); + pullRequest.setNextOffset(offset); + } + } else { + this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException); + log.info("pull message later because not locked in broker, {}", pullRequest); + return; + } + } + + final MessageQueue messageQueue = pullRequest.getMessageQueue(); + final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(messageQueue.getTopic()); + if (null == subscriptionData) { + this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException); + log.warn("find the consumer's subscription failed, {}", pullRequest); + return; + } + + final long beginTimestamp = System.currentTimeMillis(); + + PullCallback pullCallback = new PullCallback() { + @Override + public void onSuccess(PullResult pullResult) { + if (pullResult != null) { + pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult, + subscriptionData); + + switch (pullResult.getPullStatus()) { + case FOUND: + long prevRequestOffset = pullRequest.getNextOffset(); + pullRequest.setNextOffset(pullResult.getNextBeginOffset()); + long pullRT = System.currentTimeMillis() - beginTimestamp; + DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullRT(pullRequest.getConsumerGroup(), + pullRequest.getMessageQueue().getTopic(), pullRT); + + long firstMsgOffset = Long.MAX_VALUE; + if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) { + DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest); + } else { + firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset(); + + DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(), + pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size()); + + boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList()); + DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest( + pullResult.getMsgFoundList(), + processQueue, + pullRequest.getMessageQueue(), + dispatchToConsume); + + if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) { + DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, + DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval()); + } else { + DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest); + } + } + + if (pullResult.getNextBeginOffset() < prevRequestOffset + || firstMsgOffset < prevRequestOffset) { + log.warn( + "[BUG] pull message result maybe data wrong, nextBeginOffset: {} firstMsgOffset: {} prevRequestOffset: {}", + pullResult.getNextBeginOffset(), + firstMsgOffset, + prevRequestOffset); + } + + break; + case NO_NEW_MSG: + case NO_MATCHED_MSG: + pullRequest.setNextOffset(pullResult.getNextBeginOffset()); + + DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest); + + DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest); + break; + case OFFSET_ILLEGAL: + log.warn("the pull request offset illegal, {} {}", + pullRequest.toString(), pullResult.toString()); + pullRequest.setNextOffset(pullResult.getNextBeginOffset()); + + pullRequest.getProcessQueue().setDropped(true); + DefaultMQPushConsumerImpl.this.executeTask(new Runnable() { + + @Override + public void run() { + try { + 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); + } + } + }); + break; + default: + break; + } + } + } + + @Override + public void onException(Throwable e) { + if (!pullRequest.getMessageQueue().getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + 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); + } + } + + 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); + } + } + }; + + boolean commitOffsetEnable = false; + long commitOffsetValue = 0L; + if (MessageModel.CLUSTERING == this.defaultMQPushConsumer.getMessageModel()) { + commitOffsetValue = this.offsetStore.readOffset(pullRequest.getMessageQueue(), ReadOffsetType.READ_FROM_MEMORY); + if (commitOffsetValue > 0) { + commitOffsetEnable = true; + } + } + + String subExpression = null; + boolean classFilter = false; + SubscriptionData sd = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic()); + if (sd != null) { + if (this.defaultMQPushConsumer.isPostSubscriptionWhenPull() && !sd.isClassFilterMode()) { + subExpression = sd.getSubString(); + } + + classFilter = sd.isClassFilterMode(); + } + + int sysFlag = PullSysFlag.buildSysFlag( + commitOffsetEnable, // commitOffset + true, // suspend + subExpression != null, // subscription + classFilter // class filter + ); + try { + this.pullAPIWrapper.pullKernelImpl( + pullRequest.getMessageQueue(), + subExpression, + subscriptionData.getExpressionType(), + subscriptionData.getSubVersion(), + pullRequest.getNextOffset(), + this.defaultMQPushConsumer.getPullBatchSize(), + this.defaultMQPushConsumer.getPullBatchSizeInBytes(), + sysFlag, + commitOffsetValue, + BROKER_SUSPEND_MAX_TIME_MILLIS, + CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND, + CommunicationMode.ASYNC, + pullCallback + ); + } catch (Exception e) { + log.error("pullKernelImpl exception", e); + 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("popMessage 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: + 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 = new ArrayList<>(popResult.getMsgFoundList().size()); + if (!subscriptionData.getTagsSet().isEmpty() && !subscriptionData.isClassFilterMode() + && popResult.getMsgFoundList().size() > 0) { + for (MessageExt msg : popResult.getMsgFoundList()) { + if (msg.getTags() != null) { + if (subscriptionData.getTagsSet().contains(msg.getTags())) { + msgListFilterAgain.add(msg); + } + } + } + } else { + msgListFilterAgain.addAll(msgFoundList); + } + + 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()); + } + } + } + } + + Iterator iterator = msgListFilterAgain.iterator(); + while (iterator.hasNext()) { + MessageExt msg = iterator.next(); + if (msg.getReconsumeTimes() > getMaxReconsumeTimes()) { + iterator.remove(); + log.info("Reconsume times has reached {}, so ack msg={}", msg.getReconsumeTimes(), msg); + } + } + + 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, " + + this.serviceState + + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK), + null); + } + } + + void executePullRequestLater(final PullRequest pullRequest, final long timeDelay) { + this.mQClientFactory.getPullMessageService().executePullRequestLater(pullRequest, timeDelay); + } + + public boolean isPause() { + return pause; + } + + public void setPause(boolean pause) { + this.pause = pause; + } + + public ConsumerStatsManager getConsumerStatsManager() { + return this.mQClientFactory.getConsumerStatsManager(); + } + + 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); + } + } + + 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); + } + + public MessageExt queryMessageByUniqKey(String topic, String uniqKey) throws MQClientException, + InterruptedException { + return this.mQClientFactory.getMQAdminImpl().queryMessageByUniqKey(topic, uniqKey); + } + + public void registerMessageListener(MessageListener messageListener) { + this.messageListenerInner = messageListener; + } + + public void resume() { + this.pause = false; + doRebalance(); + 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, msg.getBrokerName(), mq); + } + + + private void sendMessageBack(MessageExt msg, int delayLevel, final String brokerName, final MessageQueue mq) + throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + boolean needRetry = true; + try { + 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()); + if (UtilAll.isBlank(brokerAddr)) { + throw new MQClientException("Broker[" + brokerName + "] master node does not exist", null); + } + 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); + + 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()); + + 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))); + } + + + 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); + } + + public int getMaxReconsumeTimes() { + // default reconsume times: 16 + if (this.defaultMQPushConsumer.getMaxReconsumeTimes() == -1) { + return 16; + } else { + return this.defaultMQPushConsumer.getMaxReconsumeTimes(); + } + } + + public void shutdown() { + shutdown(0); + } + + public synchronized void shutdown(long awaitTerminateMillis) { + switch (this.serviceState) { + case CREATE_JUST: + break; + case RUNNING: + this.consumeMessageService.shutdown(awaitTerminateMillis); + this.persistConsumerOffset(); + this.mQClientFactory.unregisterConsumer(this.defaultMQPushConsumer.getConsumerGroup()); + this.mQClientFactory.shutdown(); + log.info("the consumer [{}] shutdown OK", this.defaultMQPushConsumer.getConsumerGroup()); + this.rebalanceImpl.destroy(); + this.serviceState = ServiceState.SHUTDOWN_ALREADY; + break; + case SHUTDOWN_ALREADY: + break; + default: + break; + } + } + + public synchronized void start() throws MQClientException { + switch (this.serviceState) { + case CREATE_JUST: + log.info("the consumer [{}] start beginning. messageModel={}, isUnitMode={}", this.defaultMQPushConsumer.getConsumerGroup(), + this.defaultMQPushConsumer.getMessageModel(), this.defaultMQPushConsumer.isUnitMode()); + this.serviceState = ServiceState.START_FAILED; + + this.checkConfig(); + + this.copySubscription(); + + if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) { + this.defaultMQPushConsumer.changeInstanceNameToPID(); + } + + 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); + + if (this.pullAPIWrapper == null) { + this.pullAPIWrapper = new PullAPIWrapper( + mQClientFactory, + this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode()); + } + this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList); + + if (this.defaultMQPushConsumer.getOffsetStore() != null) { + this.offsetStore = this.defaultMQPushConsumer.getOffsetStore(); + } else { + switch (this.defaultMQPushConsumer.getMessageModel()) { + case BROADCASTING: + this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup()); + break; + case CLUSTERING: + this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup()); + break; + default: + break; + } + this.defaultMQPushConsumer.setOffsetStore(this.offsetStore); + } + this.offsetStore.load(); + + if (this.getMessageListenerInner() instanceof MessageListenerOrderly) { + 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(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); + } + + mQClientFactory.start(); + log.info("the consumer [{}] start OK.", this.defaultMQPushConsumer.getConsumerGroup()); + this.serviceState = ServiceState.RUNNING; + break; + case RUNNING: + case START_FAILED: + case SHUTDOWN_ALREADY: + throw new MQClientException("The PushConsumer service state not OK, maybe started once, " + + this.serviceState + + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK), + null); + default: + break; + } + + try { + this.updateTopicSubscribeInfoWhenSubscriptionChanged(); + this.mQClientFactory.checkClientInBroker(); + if (this.mQClientFactory.sendHeartbeatToAllBrokerWithLock()) { + this.mQClientFactory.rebalanceImmediately(); + } + } catch (Exception e) { + log.warn("Start the consumer {} fail.", this.defaultMQPushConsumer.getConsumerGroup(), e); + shutdown(); + throw e; + } + } + + private void checkConfig() throws MQClientException { + Validators.checkGroup(this.defaultMQPushConsumer.getConsumerGroup()); + + if (null == this.defaultMQPushConsumer.getConsumerGroup()) { + throw new MQClientException( + "consumerGroup is null" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + + if (this.defaultMQPushConsumer.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); + } + + if (null == this.defaultMQPushConsumer.getMessageModel()) { + throw new MQClientException( + "messageModel is null" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + + if (null == this.defaultMQPushConsumer.getConsumeFromWhere()) { + throw new MQClientException( + "consumeFromWhere is null" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + + Date dt = UtilAll.parseDate(this.defaultMQPushConsumer.getConsumeTimestamp(), UtilAll.YYYYMMDDHHMMSS); + if (null == dt) { + throw new MQClientException( + "consumeTimestamp is invalid, the valid format is yyyyMMddHHmmss,but received " + + this.defaultMQPushConsumer.getConsumeTimestamp() + + " " + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), null); + } + + // allocateMessageQueueStrategy + if (null == this.defaultMQPushConsumer.getAllocateMessageQueueStrategy()) { + throw new MQClientException( + "allocateMessageQueueStrategy is null" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + + // subscription + if (null == this.defaultMQPushConsumer.getSubscription()) { + throw new MQClientException( + "subscription is null" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + + // messageListener + if (null == this.defaultMQPushConsumer.getMessageListener()) { + throw new MQClientException( + "messageListener is null" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + + boolean orderly = this.defaultMQPushConsumer.getMessageListener() instanceof MessageListenerOrderly; + boolean concurrently = this.defaultMQPushConsumer.getMessageListener() instanceof MessageListenerConcurrently; + if (!orderly && !concurrently) { + throw new MQClientException( + "messageListener must be instanceof MessageListenerOrderly or MessageListenerConcurrently" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + + // consumeThreadMin + if (this.defaultMQPushConsumer.getConsumeThreadMin() < 1 + || this.defaultMQPushConsumer.getConsumeThreadMin() > 1000) { + throw new MQClientException( + "consumeThreadMin Out of range [1, 1000]" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + + // consumeThreadMax + if (this.defaultMQPushConsumer.getConsumeThreadMax() < 1 || this.defaultMQPushConsumer.getConsumeThreadMax() > 1000) { + throw new MQClientException( + "consumeThreadMax Out of range [1, 1000]" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + + // consumeThreadMin can't be larger than consumeThreadMax + if (this.defaultMQPushConsumer.getConsumeThreadMin() > this.defaultMQPushConsumer.getConsumeThreadMax()) { + throw new MQClientException( + "consumeThreadMin (" + this.defaultMQPushConsumer.getConsumeThreadMin() + ") " + + "is larger than consumeThreadMax (" + this.defaultMQPushConsumer.getConsumeThreadMax() + ")", + null); + } + + // consumeConcurrentlyMaxSpan + if (this.defaultMQPushConsumer.getConsumeConcurrentlyMaxSpan() < 1 + || this.defaultMQPushConsumer.getConsumeConcurrentlyMaxSpan() > 65535) { + throw new MQClientException( + "consumeConcurrentlyMaxSpan Out of range [1, 65535]" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + + // pullThresholdForQueue + if (this.defaultMQPushConsumer.getPullThresholdForQueue() < 1 || this.defaultMQPushConsumer.getPullThresholdForQueue() > 65535) { + throw new MQClientException( + "pullThresholdForQueue Out of range [1, 65535]" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + + // pullThresholdForTopic + if (this.defaultMQPushConsumer.getPullThresholdForTopic() != -1) { + if (this.defaultMQPushConsumer.getPullThresholdForTopic() < 1 || this.defaultMQPushConsumer.getPullThresholdForTopic() > 6553500) { + throw new MQClientException( + "pullThresholdForTopic Out of range [1, 6553500]" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + } + + // pullThresholdSizeForQueue + if (this.defaultMQPushConsumer.getPullThresholdSizeForQueue() < 1 || this.defaultMQPushConsumer.getPullThresholdSizeForQueue() > 1024) { + throw new MQClientException( + "pullThresholdSizeForQueue Out of range [1, 1024]" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + + if (this.defaultMQPushConsumer.getPullThresholdSizeForTopic() != -1) { + // pullThresholdSizeForTopic + if (this.defaultMQPushConsumer.getPullThresholdSizeForTopic() < 1 || this.defaultMQPushConsumer.getPullThresholdSizeForTopic() > 102400) { + throw new MQClientException( + "pullThresholdSizeForTopic Out of range [1, 102400]" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + } + + // pullInterval + if (this.defaultMQPushConsumer.getPullInterval() < 0 || this.defaultMQPushConsumer.getPullInterval() > 65535) { + throw new MQClientException( + "pullInterval Out of range [0, 65535]" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + + // consumeMessageBatchMaxSize + if (this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize() < 1 + || this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize() > 1024) { + throw new MQClientException( + "consumeMessageBatchMaxSize Out of range [1, 1024]" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + + // pullBatchSize + if (this.defaultMQPushConsumer.getPullBatchSize() < 1 || this.defaultMQPushConsumer.getPullBatchSize() > 1024) { + throw new MQClientException( + "pullBatchSize Out of range [1, 1024]" + + 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 { + try { + Map sub = this.defaultMQPushConsumer.getSubscription(); + if (sub != null) { + for (final Map.Entry entry : sub.entrySet()) { + final String topic = entry.getKey(); + final String subString = entry.getValue(); + SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(topic, subString); + this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData); + } + } + + if (null == this.messageListenerInner) { + this.messageListenerInner = this.defaultMQPushConsumer.getMessageListener(); + } + + switch (this.defaultMQPushConsumer.getMessageModel()) { + case BROADCASTING: + break; + case CLUSTERING: + final String retryTopic = MixAll.getRetryTopic(this.defaultMQPushConsumer.getConsumerGroup()); + SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(retryTopic, SubscriptionData.SUB_ALL); + this.rebalanceImpl.getSubscriptionInner().put(retryTopic, subscriptionData); + break; + default: + break; + } + } catch (Exception e) { + throw new MQClientException("subscription exception", e); + } + } + + public MessageListener getMessageListenerInner() { + return messageListenerInner; + } + + private void updateTopicSubscribeInfoWhenSubscriptionChanged() { + if (doNotUpdateTopicSubscribeInfoWhenSubscriptionChanged) { + return; + } + Map subTable = this.getSubscriptionInner(); + if (subTable != null) { + for (final Map.Entry entry : subTable.entrySet()) { + final String topic = entry.getKey(); + this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic); + } + } + } + + public ConcurrentMap getSubscriptionInner() { + return this.rebalanceImpl.getSubscriptionInner(); + } + + public void subscribe(String topic, String subExpression) throws MQClientException { + try { + SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(topic, subExpression); + this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData); + if (this.mQClientFactory != null) { + this.mQClientFactory.sendHeartbeatToAllBrokerWithLock(); + } + } catch (Exception e) { + throw new MQClientException("subscription exception", e); + } + } + + public void subscribe(String topic, String fullClassName, String filterClassSource) throws MQClientException { + try { + SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(topic, SubscriptionData.SUB_ALL); + subscriptionData.setSubString(fullClassName); + subscriptionData.setClassFilterMode(true); + subscriptionData.setFilterClassSource(filterClassSource); + this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData); + if (this.mQClientFactory != null) { + this.mQClientFactory.sendHeartbeatToAllBrokerWithLock(); + } + + } catch (Exception e) { + throw new MQClientException("subscription exception", e); + } + } + + public void subscribe(final String topic, final MessageSelector messageSelector) throws MQClientException { + try { + if (messageSelector == null) { + subscribe(topic, SubscriptionData.SUB_ALL); + return; + } + + SubscriptionData subscriptionData = FilterAPI.build(topic, + messageSelector.getExpression(), messageSelector.getExpressionType()); + + this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData); + if (this.mQClientFactory != null) { + this.mQClientFactory.sendHeartbeatToAllBrokerWithLock(); + } + } catch (Exception e) { + throw new MQClientException("subscription exception", e); + } + } + + public void suspend() { + this.pause = true; + log.info("suspend this consumer, {}", this.defaultMQPushConsumer.getConsumerGroup()); + } + + public void unsubscribe(String topic) { + this.rebalanceImpl.getSubscriptionInner().remove(topic); + } + + public void updateConsumeOffset(MessageQueue mq, long offset) { + this.offsetStore.updateOffset(mq, offset, false); + } + + public void updateCorePoolSize(int corePoolSize) { + this.consumeMessageService.updateCorePoolSize(corePoolSize); + } + + public MessageExt viewMessage(String topic, String msgId) + throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + return this.mQClientFactory.getMQAdminImpl().viewMessage(topic, msgId); + } + + public RebalanceImpl getRebalanceImpl() { + return rebalanceImpl; + } + + public boolean isConsumeOrderly() { + return consumeOrderly; + } + + public void setConsumeOrderly(boolean consumeOrderly) { + this.consumeOrderly = consumeOrderly; + } + + public void resetOffsetByTimeStamp(long timeStamp) throws MQClientException { + for (String topic : rebalanceImpl.getSubscriptionInner().keySet()) { + Set mqs = rebalanceImpl.getTopicSubscribeInfoTable().get(topic); + if (CollectionUtils.isNotEmpty(mqs)) { + Map offsetTable = new HashMap<>(mqs.size(), 1); + for (MessageQueue mq : mqs) { + long offset = searchOffset(mq, timeStamp); + offsetTable.put(mq, offset); + } + this.mQClientFactory.resetOffset(topic, groupName(), offsetTable); + } + } + } + + public long searchOffset(MessageQueue mq, long timestamp) throws MQClientException { + return this.mQClientFactory.getMQAdminImpl().searchOffset(mq, timestamp); + } + + @Override + public String groupName() { + return this.defaultMQPushConsumer.getConsumerGroup(); + } + + @Override + public MessageModel messageModel() { + return this.defaultMQPushConsumer.getMessageModel(); + } + + @Override + public ConsumeType consumeType() { + return ConsumeType.CONSUME_PASSIVELY; + } + + @Override + public ConsumeFromWhere consumeFromWhere() { + return this.defaultMQPushConsumer.getConsumeFromWhere(); + } + + @Override + public Set subscriptions() { + return new HashSet<>(this.rebalanceImpl.getSubscriptionInner().values()); + } + + @Override + public void doRebalance() { + if (!this.pause) { + this.rebalanceImpl.doRebalance(this.isConsumeOrderly()); + } + } + + @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 allocateMq = this.rebalanceImpl.getProcessQueueTable().keySet(); + mqs.addAll(allocateMq); + + this.offsetStore.persistAll(mqs); + } catch (Exception e) { + log.error("group: " + this.defaultMQPushConsumer.getConsumerGroup() + " persistConsumerOffset exception", e); + } + } + + @Override + public void updateTopicSubscribeInfo(String topic, Set info) { + Map subTable = this.getSubscriptionInner(); + if (subTable != null) { + if (subTable.containsKey(topic)) { + this.rebalanceImpl.topicSubscribeInfoTable.put(topic, info); + } + } + } + + @Override + public boolean isSubscribeTopicNeedUpdate(String topic) { + Map subTable = this.getSubscriptionInner(); + if (subTable != null) { + if (subTable.containsKey(topic)) { + return !this.rebalanceImpl.topicSubscribeInfoTable.containsKey(topic); + } + } + + return false; + } + + @Override + public boolean isUnitMode() { + return this.defaultMQPushConsumer.isUnitMode(); + } + + @Override + public ConsumerRunningInfo consumerRunningInfo() { + ConsumerRunningInfo info = new ConsumerRunningInfo(); + + Properties prop = MixAll.object2Properties(this.defaultMQPushConsumer); + + prop.put(ConsumerRunningInfo.PROP_CONSUME_ORDERLY, String.valueOf(this.consumeOrderly)); + prop.put(ConsumerRunningInfo.PROP_THREADPOOL_CORE_SIZE, String.valueOf(this.consumeMessageService.getCorePoolSize())); + prop.put(ConsumerRunningInfo.PROP_CONSUMER_START_TIMESTAMP, String.valueOf(this.consumerStartTimestamp)); + + info.setProperties(prop); + + Set subSet = this.subscriptions(); + info.getSubscriptionSet().addAll(subSet); + + Iterator> it = this.rebalanceImpl.getProcessQueueTable().entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + MessageQueue mq = next.getKey(); + ProcessQueue pq = next.getValue(); + + ProcessQueueInfo pqinfo = new ProcessQueueInfo(); + pqinfo.setCommitOffset(this.offsetStore.readOffset(mq, ReadOffsetType.MEMORY_FIRST_THEN_STORE)); + pq.fillProcessQueueInfo(pqinfo); + 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); + } + + return info; + } + + public MQClientInstance getmQClientFactory() { + return mQClientFactory; + } + + public void setmQClientFactory(MQClientInstance mQClientFactory) { + this.mQClientFactory = mQClientFactory; + } + + public ServiceState getServiceState() { + return serviceState; + } + + //Don't use this deprecated setter, which will be removed soon. + @Deprecated + public synchronized void setServiceState(ServiceState serviceState) { + this.serviceState = serviceState; + } + + public void adjustThreadPool() { + long computeAccTotal = this.computeAccumulationTotal(); + long adjustThreadPoolNumsThreshold = this.defaultMQPushConsumer.getAdjustThreadPoolNumsThreshold(); + + long incThreshold = (long) (adjustThreadPoolNumsThreshold * 1.0); + + long decThreshold = (long) (adjustThreadPoolNumsThreshold * 0.8); + + if (computeAccTotal >= incThreshold) { + this.consumeMessageService.incCorePoolSize(); + } + + if (computeAccTotal < decThreshold) { + this.consumeMessageService.decCorePoolSize(); + } + } + + private long computeAccumulationTotal() { + long msgAccTotal = 0; + ConcurrentMap processQueueTable = this.rebalanceImpl.getProcessQueueTable(); + Iterator> it = processQueueTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + ProcessQueue value = next.getValue(); + msgAccTotal += value.getMsgAccCnt(); + } + + return msgAccTotal; + } + + public List queryConsumeTimeSpan(final String topic) + throws RemotingException, MQClientException, InterruptedException, MQBrokerException { + List queueTimeSpan = new ArrayList<>(); + TopicRouteData routeData = this.mQClientFactory.getMQClientAPIImpl().getTopicRouteInfoFromNameServer(topic, 3000); + for (BrokerData brokerData : routeData.getBrokerDatas()) { + String addr = brokerData.selectBrokerAddr(); + queueTimeSpan.addAll(this.mQClientFactory.getMQClientAPIImpl().queryConsumeTimeSpan(addr, topic, groupName(), 3000)); + } + + 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; + } + + 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 new file mode 100644 index 0000000..8fc1cc9 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/MQConsumerInner.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.impl.consumer; + +import java.util.Set; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.MessageQueue; +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 + */ +public interface MQConsumerInner { + String groupName(); + + MessageModel messageModel(); + + ConsumeType consumeType(); + + ConsumeFromWhere consumeFromWhere(); + + Set subscriptions(); + + void doRebalance(); + + boolean tryRebalance(); + + void persistConsumerOffset(); + + void updateTopicSubscribeInfo(final String topic, final Set info); + + boolean isSubscribeTopicNeedUpdate(final String topic); + + boolean isUnitMode(); + + ConsumerRunningInfo consumerRunningInfo(); +} 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 new file mode 100644 index 0000000..0fc9c93 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/MessageQueueLock.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.impl.consumer; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.apache.rocketmq.common.message.MessageQueue; + +/** + * Message lock,strictly ensure the single queue only one thread at a time consuming + */ +public class MessageQueueLock { + private ConcurrentMap> mqLockTable = + new ConcurrentHashMap<>(32); + + public Object fetchLockObject(final MessageQueue mq) { + 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) { + lock = prevLock; + } + } + + 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 0000000..a808538 --- /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 0000000..5082754 --- /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 0000000..c47f2d0 --- /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 new file mode 100644 index 0000000..bc1b5ef --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ProcessQueue.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.client.impl.consumer; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +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.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.body.ProcessQueueInfo; + +/** + * Queue consumption snapshot + */ +public class ProcessQueue { + public final static long REBALANCE_LOCK_MAX_LIVE_TIME = + 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 = 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 ReadWriteLock consumeLock = new ReentrantReadWriteLock(); + /** + * A subset of msgTreeMap, will only be used when orderly consume + */ + private final TreeMap consumingMsgOrderlyTreeMap = new TreeMap<>(); + private final AtomicLong tryUnlockTimes = new AtomicLong(0); + private volatile long queueOffsetMax = 0L; + private volatile boolean dropped = false; + private volatile long lastPullTimestamp = System.currentTimeMillis(); + private volatile long lastConsumeTimestamp = System.currentTimeMillis(); + private volatile boolean locked = false; + private volatile long lastLockTimestamp = System.currentTimeMillis(); + private volatile boolean consuming = false; + private volatile long msgAccCnt = 0; + + public boolean isLockExpired() { + return (System.currentTimeMillis() - this.lastLockTimestamp) > REBALANCE_LOCK_MAX_LIVE_TIME; + } + + public boolean isPullExpired() { + return (System.currentTimeMillis() - this.lastPullTimestamp) > PULL_MAX_IDLE_TIME; + } + + /** + * @param pushConsumer + */ + public void cleanExpiredMsg(DefaultMQPushConsumer pushConsumer) { + if (pushConsumer.isConsumeOrderly()) { + return; + } + + int loop = Math.min(msgTreeMap.size(), 16); + for (int i = 0; i < loop; i++) { + MessageExt msg = null; + try { + this.treeMapLock.readLock().lockInterruptibly(); + try { + 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.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.treeMapLock.writeLock().lockInterruptibly(); + try { + if (!msgTreeMap.isEmpty() && msg.getQueueOffset() == msgTreeMap.firstKey()) { + try { + removeMessage(Collections.singletonList(msg)); + } catch (Exception e) { + log.error("send expired msg exception", e); + } + } + } finally { + this.treeMapLock.writeLock().unlock(); + } + } catch (InterruptedException e) { + log.error("getExpiredMsg exception", e); + } + } catch (Exception e) { + log.error("send expired msg exception", e); + } + } + } + + public boolean putMessage(final List msgs) { + boolean dispatchToConsume = false; + try { + this.treeMapLock.writeLock().lockInterruptibly(); + try { + int validMsgCnt = 0; + for (MessageExt msg : msgs) { + MessageExt old = msgTreeMap.put(msg.getQueueOffset(), msg); + if (null == old) { + validMsgCnt++; + this.queueOffsetMax = msg.getQueueOffset(); + msgSize.addAndGet(null == msg.getBody() ? 0 : msg.getBody().length); + } + } + msgCount.addAndGet(validMsgCnt); + + if (!msgTreeMap.isEmpty() && !this.consuming) { + dispatchToConsume = true; + this.consuming = true; + } + + if (!msgs.isEmpty()) { + MessageExt messageExt = msgs.get(msgs.size() - 1); + String property = messageExt.getProperty(MessageConst.PROPERTY_MAX_OFFSET); + if (property != null) { + long accTotal = Long.parseLong(property) - messageExt.getQueueOffset(); + if (accTotal > 0) { + this.msgAccCnt = accTotal; + } + } + } + } finally { + this.treeMapLock.writeLock().unlock(); + } + } catch (InterruptedException e) { + log.error("putMessage exception", e); + } + + return dispatchToConsume; + } + + public long getMaxSpan() { + try { + this.treeMapLock.readLock().lockInterruptibly(); + try { + if (!this.msgTreeMap.isEmpty()) { + return this.msgTreeMap.lastKey() - this.msgTreeMap.firstKey(); + } + } finally { + this.treeMapLock.readLock().unlock(); + } + } catch (InterruptedException e) { + log.error("getMaxSpan exception", e); + } + + return 0; + } + + public long removeMessage(final List msgs) { + long result = -1; + final long now = System.currentTimeMillis(); + try { + this.treeMapLock.writeLock().lockInterruptibly(); + this.lastConsumeTimestamp = now; + try { + if (!msgTreeMap.isEmpty()) { + result = this.queueOffsetMax + 1; + int removedCnt = 0; + for (MessageExt msg : msgs) { + MessageExt prev = msgTreeMap.remove(msg.getQueueOffset()); + if (prev != null) { + removedCnt--; + long bodySize = null == msg.getBody() ? 0 : msg.getBody().length; + if (bodySize > 0) { + msgSize.addAndGet(-bodySize); + } + } + } + if (msgCount.addAndGet(removedCnt) == 0) { + msgSize.set(0); + } + + if (!msgTreeMap.isEmpty()) { + result = msgTreeMap.firstKey(); + } + } + } finally { + this.treeMapLock.writeLock().unlock(); + } + } catch (Throwable t) { + log.error("removeMessage exception", t); + } + + return result; + } + + public TreeMap getMsgTreeMap() { + return msgTreeMap; + } + + public AtomicLong getMsgCount() { + return msgCount; + } + + public AtomicLong getMsgSize() { + return msgSize; + } + + public boolean isDropped() { + return dropped; + } + + public void setDropped(boolean dropped) { + this.dropped = dropped; + } + + public boolean isLocked() { + return locked; + } + + public void setLocked(boolean locked) { + this.locked = locked; + } + + public void rollback() { + try { + this.treeMapLock.writeLock().lockInterruptibly(); + try { + this.msgTreeMap.putAll(this.consumingMsgOrderlyTreeMap); + this.consumingMsgOrderlyTreeMap.clear(); + } finally { + this.treeMapLock.writeLock().unlock(); + } + } catch (InterruptedException e) { + log.error("rollback exception", e); + } + } + + public long commit() { + try { + this.treeMapLock.writeLock().lockInterruptibly(); + try { + Long offset = this.consumingMsgOrderlyTreeMap.lastKey(); + if (msgCount.addAndGet(-this.consumingMsgOrderlyTreeMap.size()) == 0) { + msgSize.set(0); + } else { + for (MessageExt msg : this.consumingMsgOrderlyTreeMap.values()) { + int bodySize = null == msg.getBody() ? 0 : msg.getBody().length; + if (bodySize > 0) { + msgSize.addAndGet(-bodySize); + } + } + } + this.consumingMsgOrderlyTreeMap.clear(); + if (offset != null) { + return offset + 1; + } + } finally { + this.treeMapLock.writeLock().unlock(); + } + } catch (InterruptedException e) { + log.error("commit exception", e); + } + + return -1; + } + + public void makeMessageToConsumeAgain(List msgs) { + try { + this.treeMapLock.writeLock().lockInterruptibly(); + try { + for (MessageExt msg : msgs) { + this.consumingMsgOrderlyTreeMap.remove(msg.getQueueOffset()); + this.msgTreeMap.put(msg.getQueueOffset(), msg); + } + } finally { + this.treeMapLock.writeLock().unlock(); + } + } catch (InterruptedException e) { + log.error("makeMessageToCosumeAgain exception", e); + } + } + + public List takeMessages(final int batchSize) { + List result = new ArrayList<>(batchSize); + final long now = System.currentTimeMillis(); + try { + this.treeMapLock.writeLock().lockInterruptibly(); + this.lastConsumeTimestamp = now; + try { + if (!this.msgTreeMap.isEmpty()) { + for (int i = 0; i < batchSize; i++) { + Map.Entry entry = this.msgTreeMap.pollFirstEntry(); + if (entry != null) { + result.add(entry.getValue()); + consumingMsgOrderlyTreeMap.put(entry.getKey(), entry.getValue()); + } else { + break; + } + } + } + + if (result.isEmpty()) { + consuming = false; + } + } finally { + this.treeMapLock.writeLock().unlock(); + } + } catch (InterruptedException e) { + log.error("take Messages exception", e); + } + + 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.treeMapLock.readLock().lockInterruptibly(); + try { + return !this.msgTreeMap.isEmpty(); + } finally { + this.treeMapLock.readLock().unlock(); + } + } catch (InterruptedException e) { + } + + return true; + } + + public void clear() { + try { + this.treeMapLock.writeLock().lockInterruptibly(); + try { + this.msgTreeMap.clear(); + this.consumingMsgOrderlyTreeMap.clear(); + this.msgCount.set(0); + this.msgSize.set(0); + this.queueOffsetMax = 0L; + } finally { + this.treeMapLock.writeLock().unlock(); + } + } catch (InterruptedException e) { + log.error("rollback exception", e); + } + } + + public long getLastLockTimestamp() { + return lastLockTimestamp; + } + + public void setLastLockTimestamp(long lastLockTimestamp) { + this.lastLockTimestamp = lastLockTimestamp; + } + + public ReadWriteLock getConsumeLock() { + return consumeLock; + } + + public long getLastPullTimestamp() { + return lastPullTimestamp; + } + + public void setLastPullTimestamp(long lastPullTimestamp) { + this.lastPullTimestamp = lastPullTimestamp; + } + + public long getMsgAccCnt() { + return msgAccCnt; + } + + public void setMsgAccCnt(long msgAccCnt) { + this.msgAccCnt = msgAccCnt; + } + + public long getTryUnlockTimes() { + return this.tryUnlockTimes.get(); + } + + public void incTryUnlockTimes() { + this.tryUnlockTimes.incrementAndGet(); + } + + public void fillProcessQueueInfo(final ProcessQueueInfo info) { + try { + 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))); + + if (!this.consumingMsgOrderlyTreeMap.isEmpty()) { + info.setTransactionMsgMinOffset(this.consumingMsgOrderlyTreeMap.firstKey()); + info.setTransactionMsgMaxOffset(this.consumingMsgOrderlyTreeMap.lastKey()); + info.setTransactionMsgCount(this.consumingMsgOrderlyTreeMap.size()); + } + + info.setLocked(this.locked); + info.setTryUnlockTimes(this.tryUnlockTimes.get()); + info.setLastLockTimestamp(this.lastLockTimestamp); + + info.setDroped(this.dropped); + info.setLastPullTimestamp(this.lastPullTimestamp); + info.setLastConsumeTimestamp(this.lastConsumeTimestamp); + } catch (Exception e) { + } finally { + this.treeMapLock.readLock().unlock(); + } + } + + public long getLastConsumeTimestamp() { + return lastConsumeTimestamp; + } + + 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 new file mode 100644 index 0000000..2bd0d99 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PullAPIWrapper.java @@ -0,0 +1,396 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +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; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +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.factory.MQClientInstance; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.filter.ExpressionType; +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.sysflag.MessageSysFlag; +import org.apache.rocketmq.common.sysflag.PullSysFlag; +import org.apache.rocketmq.remoting.exception.RemotingException; +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 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); + private volatile boolean connectBrokerByUser = false; + private volatile long defaultBrokerId = MixAll.MASTER_ID; + private Random random = new Random(System.nanoTime()); + private ArrayList filterMessageHookList = new ArrayList<>(); + + public PullAPIWrapper(MQClientInstance mQClientFactory, String consumerGroup, boolean unitMode) { + this.mQClientFactory = mQClientFactory; + this.consumerGroup = consumerGroup; + this.unitMode = unitMode; + } + + public PullResult processPullResult(final MessageQueue mq, final PullResult pullResult, + final SubscriptionData subscriptionData) { + PullResultExt pullResultExt = (PullResultExt) pullResult; + + this.updatePullFromWhichNode(mq, pullResultExt.getSuggestWhichBrokerId()); + if (PullStatus.FOUND == pullResult.getPullStatus()) { + ByteBuffer byteBuffer = ByteBuffer.wrap(pullResultExt.getMessageBinary()); + 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()); + for (MessageExt msg : msgList) { + if (msg.getTags() != null) { + if (subscriptionData.getTagsSet().contains(msg.getTags())) { + msgListFilterAgain.add(msg); + } + } + } + } + + if (this.hasHook()) { + FilterMessageContext filterMessageContext = new FilterMessageContext(); + filterMessageContext.setUnitMode(unitMode); + filterMessageContext.setMsgList(msgListFilterAgain); + this.executeHook(filterMessageContext); + } + + 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); + } + + pullResultExt.setMessageBinary(null); + + return pullResult; + } + + public void updatePullFromWhichNode(final MessageQueue mq, final long brokerId) { + AtomicLong suggest = this.pullFromWhichNodeTable.get(mq); + if (null == suggest) { + this.pullFromWhichNodeTable.put(mq, new AtomicLong(brokerId)); + } else { + suggest.set(brokerId); + } + } + + public boolean hasHook() { + return !this.filterMessageHookList.isEmpty(); + } + + public void executeHook(final FilterMessageContext context) { + if (!this.filterMessageHookList.isEmpty()) { + for (FilterMessageHook hook : this.filterMessageHookList) { + try { + hook.filterMessage(context); + } catch (Throwable e) { + log.error("execute hook error. hookName={}", hook.hookName()); + } + } + } + } + + public PullResult pullKernelImpl( + final MessageQueue mq, + final String subExpression, + final String expressionType, + final long subVersion, + final long offset, + final int maxNums, + final int maxSizeInBytes, + final int sysFlag, + final long commitOffset, + final long brokerSuspendMaxTimeMillis, + final long timeoutMillis, + final CommunicationMode communicationMode, + final PullCallback pullCallback + ) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + FindBrokerResult findBrokerResult = + this.mQClientFactory.findBrokerAddressInSubscribe(this.mQClientFactory.getBrokerNameFromMessageQueue(mq), + this.recalculatePullFromWhichNode(mq), false); + if (null == findBrokerResult) { + this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic()); + findBrokerResult = + this.mQClientFactory.findBrokerAddressInSubscribe(this.mQClientFactory.getBrokerNameFromMessageQueue(mq), + this.recalculatePullFromWhichNode(mq), false); + } + + + if (findBrokerResult != null) { + { + // check version + if (!ExpressionType.isTagType(expressionType) + && findBrokerResult.getBrokerVersion() < MQVersion.Version.V4_1_0_SNAPSHOT.ordinal()) { + throw new MQClientException("The broker[" + mq.getBrokerName() + ", " + + findBrokerResult.getBrokerVersion() + "] does not upgrade to support for filter message by " + expressionType, null); + } + } + int sysFlagInner = sysFlag; + + if (findBrokerResult.isSlave()) { + sysFlagInner = PullSysFlag.clearCommitOffsetFlag(sysFlagInner); + } + + PullMessageRequestHeader requestHeader = new PullMessageRequestHeader(); + requestHeader.setConsumerGroup(this.consumerGroup); + requestHeader.setTopic(mq.getTopic()); + requestHeader.setQueueId(mq.getQueueId()); + requestHeader.setQueueOffset(offset); + requestHeader.setMaxMsgNums(maxNums); + requestHeader.setSysFlag(sysFlagInner); + requestHeader.setCommitOffset(commitOffset); + 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 = computePullFromWhichFilterServer(mq.getTopic(), brokerAddr); + } + + PullResult pullResult = this.mQClientFactory.getMQClientAPIImpl().pullMessage( + brokerAddr, + requestHeader, + timeoutMillis, + communicationMode, + pullCallback); + + return pullResult; + } + + throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null); + } + + public PullResult pullKernelImpl( + MessageQueue mq, + final String subExpression, + final String expressionType, + final long subVersion, + long offset, + final int maxNums, + final int sysFlag, + long commitOffset, + final long brokerSuspendMaxTimeMillis, + final long timeoutMillis, + final CommunicationMode communicationMode, + PullCallback pullCallback + ) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + return pullKernelImpl( + mq, + subExpression, + expressionType, + subVersion, offset, + maxNums, + Integer.MAX_VALUE, + sysFlag, + commitOffset, + brokerSuspendMaxTimeMillis, + timeoutMillis, + communicationMode, + pullCallback + ); + } + + public long recalculatePullFromWhichNode(final MessageQueue mq) { + if (this.isConnectBrokerByUser()) { + return this.defaultBrokerId; + } + + AtomicLong suggest = this.pullFromWhichNodeTable.get(mq); + if (suggest != null) { + return suggest.get(); + } + + return MixAll.MASTER_ID; + } + + private String computePullFromWhichFilterServer(final String topic, final String brokerAddr) + throws MQClientException { + ConcurrentMap topicRouteTable = this.mQClientFactory.getTopicRouteTable(); + if (topicRouteTable != null) { + TopicRouteData topicRouteData = topicRouteTable.get(topic); + List list = topicRouteData.getFilterServerTable().get(brokerAddr); + + if (list != null && !list.isEmpty()) { + return list.get(randomNum() % list.size()); + } + } + + throw new MQClientException("Find Filter Server Failed, Broker Addr: " + brokerAddr + " topic: " + + topic, null); + } + + public boolean isConnectBrokerByUser() { + return connectBrokerByUser; + } + + public void setConnectBrokerByUser(boolean connectBrokerByUser) { + this.connectBrokerByUser = connectBrokerByUser; + + } + + public int randomNum() { + int value = random.nextInt(); + if (value < 0) { + value = Math.abs(value); + if (value < 0) + value = 0; + } + return value; + } + + public void registerFilterMessageHook(ArrayList filterMessageHookList) { + this.filterMessageHookList = filterMessageHookList; + } + + public long getDefaultBrokerId() { + return defaultBrokerId; + } + + 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 new file mode 100644 index 0000000..4300f20 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PullMessageService.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.client.impl.consumer; + +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +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.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class PullMessageService extends ServiceThread { + 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 ThreadFactoryImpl("PullMessageServiceScheduledThread")); + + public PullMessageService(MQClientInstance mQClientFactory) { + this.mQClientFactory = mQClientFactory; + } + + public void executePullRequestLater(final PullRequest pullRequest, final long timeDelay) { + 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.messageRequestQueue.put(pullRequest); + } catch (InterruptedException 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) { + 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() { + return scheduledExecutorService; + } + + private void pullMessage(final PullRequest pullRequest) { + final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup()); + if (consumer != null) { + DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer; + impl.pullMessage(pullRequest); + } else { + 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() { + logger.info(this.getServiceName() + " service started"); + + while (!this.isStopped()) { + try { + MessageRequest messageRequest = this.messageRequestQueue.take(); + if (messageRequest.getMessageRequestMode() == MessageRequestMode.POP) { + this.popMessage((PopRequest) messageRequest); + } else { + this.pullMessage((PullRequest) messageRequest); + } + } catch (InterruptedException ignored) { + } catch (Throwable e) { + logger.error("Pull Message Service Run Method exception", e); + } + } + + logger.info(this.getServiceName() + " service end"); + } + + @Override + public void shutdown(boolean interrupt) { + super.shutdown(interrupt); + ThreadUtils.shutdownGracefully(this.scheduledExecutorService, 1000, TimeUnit.MILLISECONDS); + } + + @Override + public String getServiceName() { + return PullMessageService.class.getSimpleName(); + } + +} 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 new file mode 100644 index 0000000..b90192b --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PullRequest.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.impl.consumer; + +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.message.MessageRequestMode; + +public class PullRequest implements MessageRequest { + private String consumerGroup; + private MessageQueue messageQueue; + private ProcessQueue processQueue; + private long nextOffset; + private boolean previouslyLocked = false; + + public boolean isPreviouslyLocked() { + return previouslyLocked; + } + + public void setPreviouslyLocked(boolean previouslyLocked) { + this.previouslyLocked = previouslyLocked; + } + + 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 long getNextOffset() { + return nextOffset; + } + + public void setNextOffset(long nextOffset) { + this.nextOffset = nextOffset; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + 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; + PullRequest other = (PullRequest) obj; + 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 "PullRequest [consumerGroup=" + consumerGroup + ", messageQueue=" + messageQueue + + ", nextOffset=" + nextOffset + "]"; + } + + public ProcessQueue getProcessQueue() { + return processQueue; + } + + 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 new file mode 100644 index 0000000..45538ed --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PullResultExt.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.client.impl.consumer; + +import java.util.List; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.consumer.PullStatus; +import org.apache.rocketmq.common.message.MessageExt; + +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() { + return messageBinary; + } + + public void setMessageBinary(byte[] messageBinary) { + this.messageBinary = messageBinary; + } + + public long getSuggestWhichBrokerId() { + return suggestWhichBrokerId; + } +} 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 new file mode 100644 index 0000000..b6f1d99 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalanceImpl.java @@ -0,0 +1,802 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +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.common.KeyBuilder; +import org.apache.rocketmq.common.MixAll; +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.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 = LoggerFactory.getLogger(RebalanceImpl.class); + + protected final ConcurrentMap processQueueTable = new ConcurrentHashMap<>(64); + protected final ConcurrentMap popProcessQueueTable = new ConcurrentHashMap<>(64); + + protected final ConcurrentMap> topicSubscribeInfoTable = + new ConcurrentHashMap<>(); + protected final ConcurrentMap subscriptionInner = + new ConcurrentHashMap<>(); + protected String consumerGroup; + protected MessageModel messageModel; + protected AllocateMessageQueueStrategy allocateMessageQueueStrategy; + protected MQClientInstance mQClientFactory; + private static final int QUERY_ASSIGNMENT_TIMEOUT = 3000; + + public RebalanceImpl(String consumerGroup, MessageModel messageModel, + AllocateMessageQueueStrategy allocateMessageQueueStrategy, + MQClientInstance mQClientFactory) { + this.consumerGroup = consumerGroup; + this.messageModel = messageModel; + this.allocateMessageQueueStrategy = allocateMessageQueueStrategy; + this.mQClientFactory = mQClientFactory; + } + + public void unlock(final MessageQueue mq, final boolean oneway) { + FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(this.mQClientFactory.getBrokerNameFromMessageQueue(mq), MixAll.MASTER_ID, true); + if (findBrokerResult != null) { + UnlockBatchRequestBody requestBody = new UnlockBatchRequestBody(); + requestBody.setConsumerGroup(this.consumerGroup); + requestBody.setClientId(this.mQClientFactory.getClientId()); + requestBody.getMqSet().add(mq); + + try { + this.mQClientFactory.getMQClientAPIImpl().unlockBatchMQ(findBrokerResult.getBrokerAddr(), requestBody, 1000, oneway); + log.warn("unlock messageQueue. group:{}, clientId:{}, mq:{}", + this.consumerGroup, + this.mQClientFactory.getClientId(), + mq); + } catch (Exception e) { + log.error("unlockBatchMQ exception, " + mq, e); + } + } + } + + public void unlockAll(final boolean oneway) { + HashMap> brokerMqs = this.buildProcessQueueTableByBrokerName(); + + for (final Map.Entry> entry : brokerMqs.entrySet()) { + final String brokerName = entry.getKey(); + final Set mqs = entry.getValue(); + + if (mqs.isEmpty()) { + continue; + } + + FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(brokerName, MixAll.MASTER_ID, true); + if (findBrokerResult != null) { + UnlockBatchRequestBody requestBody = new UnlockBatchRequestBody(); + requestBody.setConsumerGroup(this.consumerGroup); + requestBody.setClientId(this.mQClientFactory.getClientId()); + requestBody.setMqSet(mqs); + + try { + this.mQClientFactory.getMQClientAPIImpl().unlockBatchMQ(findBrokerResult.getBrokerAddr(), requestBody, 1000, oneway); + + for (MessageQueue mq : mqs) { + ProcessQueue processQueue = this.processQueueTable.get(mq); + if (processQueue != null) { + processQueue.setLocked(false); + log.info("the message queue unlock OK, Group: {} {}", this.consumerGroup, mq); + } + } + } catch (Exception e) { + log.error("unlockBatchMQ exception, " + mqs, e); + } + } + } + } + + private HashMap> buildProcessQueueTableByBrokerName() { + 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<>(); + result.put(mq.getBrokerName(), mqs); + } + + mqs.add(mq); + } + + return result; + } + + public boolean lock(final MessageQueue mq) { + FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(this.mQClientFactory.getBrokerNameFromMessageQueue(mq), MixAll.MASTER_ID, true); + if (findBrokerResult != null) { + LockBatchRequestBody requestBody = new LockBatchRequestBody(); + requestBody.setConsumerGroup(this.consumerGroup); + requestBody.setClientId(this.mQClientFactory.getClientId()); + requestBody.getMqSet().add(mq); + + try { + Set lockedMq = + this.mQClientFactory.getMQClientAPIImpl().lockBatchMQ(findBrokerResult.getBrokerAddr(), requestBody, 1000); + for (MessageQueue mmqq : lockedMq) { + ProcessQueue processQueue = this.processQueueTable.get(mmqq); + if (processQueue != null) { + processQueue.setLocked(true); + processQueue.setLastLockTimestamp(System.currentTimeMillis()); + } + } + + boolean lockOK = lockedMq.contains(mq); + log.info("message queue lock {}, {} {}", lockOK ? "OK" : "Failed", this.consumerGroup, mq); + return lockOK; + } catch (Exception e) { + log.error("lockBatchMQ exception, " + mq, e); + } + } + + return false; + } + + public void lockAll() { + HashMap> brokerMqs = this.buildProcessQueueTableByBrokerName(); + + Iterator>> it = brokerMqs.entrySet().iterator(); + while (it.hasNext()) { + Entry> entry = it.next(); + final String brokerName = entry.getKey(); + final Set mqs = entry.getValue(); + + if (mqs.isEmpty()) { + continue; + } + + FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(brokerName, MixAll.MASTER_ID, true); + if (findBrokerResult != null) { + LockBatchRequestBody requestBody = new LockBatchRequestBody(); + requestBody.setConsumerGroup(this.consumerGroup); + requestBody.setClientId(this.mQClientFactory.getClientId()); + requestBody.setMqSet(mqs); + + try { + Set lockOKMQSet = + this.mQClientFactory.getMQClientAPIImpl().lockBatchMQ(findBrokerResult.getBrokerAddr(), requestBody, 1000); + + for (MessageQueue mq : mqs) { + 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); + } + } + } + } catch (Exception e) { + log.error("lockBatchMQ exception, " + mqs, e); + } + } + } + } + + 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 { + if (!clientRebalance(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("rebalance Exception", e); + balanced = false; + } + } + } + } + + this.truncateMessageQueueNotMyTopic(); + + return balanced; + } + + public ConcurrentMap getSubscriptionInner() { + return subscriptionInner; + } + + private boolean rebalanceByTopic(final String topic, final boolean isOrder) { + boolean balanced = true; + switch (messageModel) { + case BROADCASTING: { + Set mqSet = this.topicSubscribeInfoTable.get(topic); + if (mqSet != null) { + boolean changed = this.updateProcessQueueTableInRebalance(topic, mqSet, false); + if (changed) { + this.messageQueueChanged(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; + } + case CLUSTERING: { + Set mqSet = this.topicSubscribeInfoTable.get(topic); + 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); + } + } + + if (null == cidAll) { + log.warn("doRebalance, {} {}, get consumer id list failed", consumerGroup, topic); + } + + if (mqSet != null && cidAll != null) { + List mqAll = new ArrayList<>(); + mqAll.addAll(mqSet); + + Collections.sort(mqAll); + Collections.sort(cidAll); + + AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy; + + List allocateResult = null; + try { + allocateResult = strategy.allocate( + this.consumerGroup, + this.mQClientFactory.getClientId(), + mqAll, + cidAll); + } catch (Throwable e) { + log.error("allocate message queue exception. strategy name: {}, ex: {}", strategy.getName(), e); + return false; + } + + Set allocateResultSet = new HashSet<>(); + if (allocateResult != null) { + allocateResultSet.addAll(allocateResult); + } + + boolean changed = this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder); + if (changed) { + log.info( + "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() { + Map subTable = this.getSubscriptionInner(); + + for (MessageQueue mq : this.processQueueTable.keySet()) { + if (!subTable.containsKey(mq.getTopic())) { + + ProcessQueue pq = this.processQueueTable.remove(mq); + if (pq != null) { + pq.setDropped(true); + log.info("doRebalance, {}, truncateMessageQueueNotMyTopic remove unnecessary mq, {}", consumerGroup, mq); + } + } + } + + 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); + } + } + } + } + + private boolean updateProcessQueueTableInRebalance(final String topic, final Set mqSet, + final boolean needLockMq) { + 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(); + MessageQueue mq = next.getKey(); + ProcessQueue pq = next.getValue(); + + if (mq.getTopic().equals(topic)) { + if (!mqSet.contains(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); + } + } + + // add new message queue + boolean allMQLocked = true; + List pullRequestList = new ArrayList<>(); + for (MessageQueue mq : mqSet) { + if (!this.processQueueTable.containsKey(mq)) { + if (needLockMq && !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 = this.computePullFromWhere(mq); + 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); + + 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); + } + } + } + + { + // 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; + } + + public abstract void messageQueueChanged(final String topic, final Set mqAll, + final Set mqDivided); + + public abstract boolean removeUnnecessaryMessageQueue(final MessageQueue mq, final ProcessQueue pq); + + public boolean removeUnnecessaryPopMessageQueue(final MessageQueue mq, final PopProcessQueue pq) { + return true; + } + + public abstract ConsumeType consumeType(); + + public abstract void removeDirtyOffset(final MessageQueue mq); + + /** + * When the network is unstable, using this interface may return wrong offset. + * It is recommended to use computePullFromWhereWithException instead. + * @param mq + * @return offset + */ + @Deprecated + public abstract long computePullFromWhere(final MessageQueue mq); + + 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 void removeProcessQueue(final MessageQueue mq) { + ProcessQueue prev = this.processQueueTable.remove(mq); + if (prev != null) { + boolean droped = prev.isDropped(); + prev.setDropped(true); + this.removeUnnecessaryMessageQueue(mq, prev); + log.info("Fix Offset, {}, remove unnecessary mq, {} Droped: {}", consumerGroup, mq, droped); + } + } + + public ConcurrentMap getProcessQueueTable() { + return processQueueTable; + } + + public ConcurrentMap getPopProcessQueueTable() { + return popProcessQueueTable; + } + + public ConcurrentMap> getTopicSubscribeInfoTable() { + return topicSubscribeInfoTable; + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public MessageModel getMessageModel() { + return messageModel; + } + + public void setMessageModel(MessageModel messageModel) { + this.messageModel = messageModel; + } + + public AllocateMessageQueueStrategy getAllocateMessageQueueStrategy() { + return allocateMessageQueueStrategy; + } + + public void setAllocateMessageQueueStrategy(AllocateMessageQueueStrategy allocateMessageQueueStrategy) { + this.allocateMessageQueueStrategy = allocateMessageQueueStrategy; + } + + public MQClientInstance getmQClientFactory() { + return mQClientFactory; + } + + public void setmQClientFactory(MQClientInstance mQClientFactory) { + this.mQClientFactory = mQClientFactory; + } + + public void destroy() { + Iterator> it = this.processQueueTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + next.getValue().setDropped(true); + } + + 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 0000000..330772f --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalanceLitePullImpl.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.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; + } + +} 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 new file mode 100644 index 0000000..e0b6828 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalancePullImpl.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.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.exception.MQClientException; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +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 RebalancePullImpl extends RebalanceImpl { + private final DefaultMQPullConsumerImpl defaultMQPullConsumerImpl; + + public RebalancePullImpl(DefaultMQPullConsumerImpl defaultMQPullConsumerImpl) { + this(null, null, null, null, defaultMQPullConsumerImpl); + } + + public RebalancePullImpl(String consumerGroup, MessageModel messageModel, + AllocateMessageQueueStrategy allocateMessageQueueStrategy, + MQClientInstance mQClientFactory, DefaultMQPullConsumerImpl defaultMQPullConsumerImpl) { + super(consumerGroup, messageModel, allocateMessageQueueStrategy, mQClientFactory); + this.defaultMQPullConsumerImpl = defaultMQPullConsumerImpl; + } + + @Override + public void messageQueueChanged(String topic, Set mqAll, Set mqDivided) { + MessageQueueListener messageQueueListener = this.defaultMQPullConsumerImpl.getDefaultMQPullConsumer().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.defaultMQPullConsumerImpl.getOffsetStore().persist(mq); + this.defaultMQPullConsumerImpl.getOffsetStore().removeOffset(mq); + return true; + } + + @Override + public ConsumeType consumeType() { + return ConsumeType.CONSUME_ACTIVELY; + } + + @Override + public void removeDirtyOffset(final MessageQueue mq) { + this.defaultMQPullConsumerImpl.getOffsetStore().removeOffset(mq); + } + + @Deprecated + @Override + public long computePullFromWhere(MessageQueue mq) { + return 0; + } + + @Override + 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; + } + +} 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 new file mode 100644 index 0000000..fe2f19b --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalancePushImpl.java @@ -0,0 +1,291 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 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.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); + } + + public RebalancePushImpl(String consumerGroup, MessageModel messageModel, + AllocateMessageQueueStrategy allocateMessageQueueStrategy, + MQClientInstance mQClientFactory, DefaultMQPushConsumerImpl defaultMQPushConsumerImpl) { + super(consumerGroup, messageModel, allocateMessageQueueStrategy, mQClientFactory); + this.defaultMQPushConsumerImpl = defaultMQPushConsumerImpl; + } + + @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. + */ + SubscriptionData subscriptionData = this.subscriptionInner.get(topic); + long newVersion = System.currentTimeMillis(); + log.info("{} Rebalance changed, also update version: {}, {}", topic, subscriptionData.getSubVersion(), newVersion); + subscriptionData.setSubVersion(newVersion); + + int currentQueueCount = this.processQueueTable.size(); + if (currentQueueCount != 0) { + int pullThresholdForTopic = this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer().getPullThresholdForTopic(); + if (pullThresholdForTopic != -1) { + int newVal = Math.max(1, pullThresholdForTopic / currentQueueCount); + log.info("The pullThresholdForQueue is changed from {} to {}", + this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer().getPullThresholdForQueue(), newVal); + this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer().setPullThresholdForQueue(newVal); + } + + int pullThresholdSizeForTopic = this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer().getPullThresholdSizeForTopic(); + if (pullThresholdSizeForTopic != -1) { + int newVal = Math.max(1, pullThresholdSizeForTopic / currentQueueCount); + log.info("The pullThresholdSizeForQueue is changed from {} to {}", + this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer().getPullThresholdSizeForQueue(), newVal); + this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer().setPullThresholdSizeForQueue(newVal); + } + } + + // notify broker + this.getmQClientFactory().sendHeartbeatToAllBrokerWithLockV2(true); + + MessageQueueListener messageQueueListener = defaultMQPushConsumerImpl.getMessageQueueListener(); + if (null != messageQueueListener) { + messageQueueListener.messageQueueChanged(topic, mqAll, mqDivided); + } + } + + @Override + public boolean removeUnnecessaryMessageQueue(final MessageQueue mq, final ProcessQueue pq) { + if (this.defaultMQPushConsumerImpl.isConsumeOrderly() + && MessageModel.CLUSTERING.equals(this.defaultMQPushConsumerImpl.messageModel())) { + + // commit offset immediately + this.defaultMQPushConsumerImpl.getOffsetStore().persist(mq); + + // remove order message queue: unlock & remove + return tryRemoveOrderMessageQueue(mq, pq); + } else { + this.defaultMQPushConsumerImpl.getOffsetStore().persist(mq); + this.defaultMQPushConsumerImpl.getOffsetStore().removeOffset(mq); + return true; + } + } + + private boolean tryRemoveOrderMessageQueue(final MessageQueue mq, final ProcessQueue pq) { + try { + // unlock & remove when no message is consuming or UNLOCK_DELAY_TIME_MILLS timeout (Backwards compatibility) + boolean forceUnlock = pq.isDropped() && System.currentTimeMillis() > 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); + + pq.setLocked(false); + RebalancePushImpl.this.unlock(mq, true); + return true; + } finally { + if (!forceUnlock) { + pq.getConsumeLock().writeLock().unlock(); + } + } + } 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); + } + + @Override + public ConsumeType consumeType() { + return ConsumeType.CONSUME_PASSIVELY; + } + + @Override + 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(); + switch (consumeFromWhere) { + case CONSUME_FROM_LAST_OFFSET_AND_FROM_MIN_WHEN_BOOT_FIRST: + case CONSUME_FROM_MIN_OFFSET: + case CONSUME_FROM_MAX_OFFSET: + case CONSUME_FROM_LAST_OFFSET: { + long lastOffset = offsetStore.readOffset(mq, ReadOffsetType.READ_FROM_STORE); + if (lastOffset >= 0) { + result = lastOffset; + } + // First start,no offset + else if (-1 == lastOffset) { + if (mq.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + 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 { + throw new MQClientException(ResponseCode.QUERY_NOT_FOUND, "Failed to query consume offset from " + + "offset store"); + } + break; + } + case CONSUME_FROM_FIRST_OFFSET: { + long lastOffset = offsetStore.readOffset(mq, ReadOffsetType.READ_FROM_STORE); + if (lastOffset >= 0) { + result = lastOffset; + } else if (-1 == lastOffset) { + //the offset will be fixed by the OFFSET_ILLEGAL process + result = 0L; + } else { + throw new MQClientException(ResponseCode.QUERY_NOT_FOUND, "Failed to query offset from offset " + + "store"); + } + break; + } + case CONSUME_FROM_TIMESTAMP: { + long lastOffset = offsetStore.readOffset(mq, ReadOffsetType.READ_FROM_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.defaultMQPushConsumerImpl.getDefaultMQPushConsumer().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 { + throw new MQClientException(ResponseCode.QUERY_NOT_FOUND, "Failed to query offset from offset " + + "store"); + } + break; + } + + default: + break; + } + + if (result < 0) { + throw new MQClientException(ResponseCode.SYSTEM_ERROR, "Found unexpected result " + result); + } + + return result; + } + + @Override + 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) { + 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 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 new file mode 100644 index 0000000..8e586c8 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalanceService.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.client.impl.consumer; + +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.common.ServiceThread; +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 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; + } + + @Override + public void run() { + log.info(this.getServiceName() + " service started"); + + long realWaitInterval = waitInterval; + while (!this.isStopped()) { + 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"); + } + + @Override + public String getServiceName() { + return RebalanceService.class.getSimpleName(); + } +} 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 new file mode 100644 index 0000000..3055f2c --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/factory/MQClientInstance.java @@ -0,0 +1,1414 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.factory; + +import com.alibaba.fastjson.JSON; +import io.netty.channel.Channel; +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.Map.Entry; +import java.util.Random; +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.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +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; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.impl.ClientRemotingProcessor; +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.DefaultMQPullConsumerImpl; +import org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl; +import org.apache.rocketmq.client.impl.consumer.MQConsumerInner; +import org.apache.rocketmq.client.impl.consumer.ProcessQueue; +import org.apache.rocketmq.client.impl.consumer.PullMessageService; +import org.apache.rocketmq.client.impl.consumer.RebalanceService; +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.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.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.message.MessageQueueAssignment; +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.RPCHook; +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.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 static org.apache.rocketmq.remoting.rpc.ClientMetadata.topicRouteData2EndpointsForStaticTopic; + +public class MQClientInstance { + private final static long LOCK_TIMEOUT_MILLIS = 3000; + private final static Logger log = LoggerFactory.getLogger(MQClientInstance.class); + private final ClientConfig clientConfig; + private final String clientId; + private final long bootTimestamp = System.currentTimeMillis(); + + /** + * 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> topicEndPointsTable = new ConcurrentHashMap<>(); + private final Lock lockNamesrv = new ReentrantLock(); + private final Lock lockHeartbeat = new ReentrantLock(); + + /** + * The container which stores the brokerClusterInfo. The key of the map is the broker 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, "MQClientFactoryFetchRemoteConfigScheduledThread"); + } + }); + 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 final Random random = new Random(); + + public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId) { + this(clientConfig, instanceIndex, clientId, null); + } + + public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) { + this.clientConfig = clientConfig; + this.nettyClientConfig = new NettyClientConfig(); + this.nettyClientConfig.setClientCallbackExecutorThreads(clientConfig.getClientCallbackExecutorThreads()); + this.nettyClientConfig.setUseTLS(clientConfig.isUseTLS()); + this.nettyClientConfig.setSocksProxyConfig(clientConfig.getSocksProxyConfig()); + this.nettyClientConfig.setScanAvailableNameSrv(false); + 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, false)) { + 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()); + log.info("user specified name server address: {}", this.clientConfig.getNamesrvAddr()); + } + + this.clientId = clientId; + + this.mQAdminImpl = new MQAdminImpl(this); + + this.pullMessageService = new PullMessageService(this); + + this.rebalanceService = new RebalanceService(this); + + this.defaultMQProducer = new DefaultMQProducer(MixAll.CLIENT_INNER_PRODUCER_GROUP); + this.defaultMQProducer.resetClientConfig(clientConfig); + + this.consumerStatsManager = new ConsumerStatsManager(this.scheduledExecutorService); + + log.info("Created a new client Instance, InstanceIndex:{}, ClientID:{}, ClientConfig:{}, ClientVersion:{}, SerializerType:{}", + instanceIndex, + this.clientId, + this.clientConfig, + MQVersion.getVersionDesc(MQVersion.CURRENT_VERSION), RemotingCommand.getSerializeTypeConfigInThisServer()); + } + + 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(";"); + for (String broker : brokers) { + String[] item = broker.split(":"); + int nums = Integer.parseInt(item[1]); + for (int i = 0; i < nums; i++) { + MessageQueue mq = new MessageQueue(topic, item[0], i); + info.getMessageQueueList().add(mq); + } + } + + 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); + for (QueueData qd : qds) { + if (PermName.isWriteable(qd.getPerm())) { + BrokerData brokerData = null; + for (BrokerData bd : route.getBrokerDatas()) { + if (bd.getBrokerName().equals(qd.getBrokerName())) { + brokerData = bd; + break; + } + } + + if (null == brokerData) { + continue; + } + + if (!brokerData.getBrokerAddrs().containsKey(MixAll.MASTER_ID)) { + continue; + } + + for (int i = 0; i < qd.getWriteQueueNums(); i++) { + MessageQueue mq = new MessageQueue(topic, qd.getBrokerName(), i); + info.getMessageQueueList().add(mq); + } + } + } + + info.setOrderTopic(false); + } + + return info; + } + + public static Set topicRouteData2TopicSubscribeInfo(final String topic, final TopicRouteData route) { + 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())) { + for (int i = 0; i < qd.getReadQueueNums(); i++) { + MessageQueue mq = new MessageQueue(topic, qd.getBrokerName(), i); + mqList.add(mq); + } + } + } + + return mqList; + } + + public void start() throws MQClientException { + + synchronized (this) { + switch (this.serviceState) { + case CREATE_JUST: + this.serviceState = ServiceState.START_FAILED; + // If not specified,looking address from name server + if (null == this.clientConfig.getNamesrvAddr()) { + this.mQClientAPIImpl.fetchNameServerAddr(); + } + // Start request-response channel + this.mQClientAPIImpl.start(); + // Start various schedule tasks + this.startScheduledTask(); + // Start pull service + this.pullMessageService.start(); + // Start rebalance service + this.rebalanceService.start(); + // Start push service + this.defaultMQProducer.getDefaultMQProducerImpl().start(false); + log.info("the client factory [{}] start OK", this.clientId); + this.serviceState = ServiceState.RUNNING; + break; + case START_FAILED: + throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null); + default: + break; + } + } + } + + private void startScheduledTask() { + if (null == this.clientConfig.getNamesrvAddr()) { + this.scheduledExecutorService.scheduleAtFixedRate(() -> { + try { + MQClientInstance.this.mQClientAPIImpl.fetchNameServerAddr(); + } catch (Throwable t) { + log.error("ScheduledTask fetchNameServerAddr exception", t); + } + }, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS); + } + + this.scheduledExecutorService.scheduleAtFixedRate(() -> { + try { + MQClientInstance.this.updateTopicRouteInfoFromNameServer(); + } catch (Throwable t) { + log.error("ScheduledTask updateTopicRouteInfoFromNameServer exception", t); + } + }, 10, this.clientConfig.getPollNameServerInterval(), TimeUnit.MILLISECONDS); + + this.scheduledExecutorService.scheduleAtFixedRate(() -> { + try { + MQClientInstance.this.cleanOfflineBroker(); + MQClientInstance.this.sendHeartbeatToAllBrokerWithLock(); + } catch (Throwable t) { + log.error("ScheduledTask sendHeartbeatToAllBroker exception", t); + } + }, 1000, this.clientConfig.getHeartbeatBrokerInterval(), TimeUnit.MILLISECONDS); + + this.scheduledExecutorService.scheduleAtFixedRate(() -> { + try { + MQClientInstance.this.persistAllConsumerOffset(); + } catch (Throwable t) { + log.error("ScheduledTask persistAllConsumerOffset exception", t); + } + }, 1000 * 10, this.clientConfig.getPersistConsumerOffsetInterval(), TimeUnit.MILLISECONDS); + + this.scheduledExecutorService.scheduleAtFixedRate(() -> { + try { + MQClientInstance.this.adjustThreadPool(); + } catch (Throwable t) { + log.error("ScheduledTask adjustThreadPool exception", t); + } + }, 1, 1, TimeUnit.MINUTES); + } + + public String getClientId() { + return clientId; + } + + public void updateTopicRouteInfoFromNameServer() { + Set topicList = new HashSet<>(); + + // Consumer + { + for (Entry entry : this.consumerTable.entrySet()) { + MQConsumerInner impl = entry.getValue(); + if (impl != null) { + Set subList = impl.subscriptions(); + if (subList != null) { + for (SubscriptionData subData : subList) { + topicList.add(subData.getTopic()); + } + } + } + } + } + + // Producer + { + for (Entry entry : this.producerTable.entrySet()) { + MQProducerInner impl = entry.getValue(); + if (impl != null) { + Set lst = impl.getPublishTopicList(); + topicList.addAll(lst); + } + } + } + + for (String topic : topicList) { + this.updateTopicRouteInfoFromNameServer(topic); + } + } + + 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 + */ + private void cleanOfflineBroker() { + try { + if (this.lockNamesrv.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) + try { + ConcurrentHashMap> updatedTable = new ConcurrentHashMap<>(this.brokerAddrTable.size(), 1); + + Iterator>> itBrokerTable = this.brokerAddrTable.entrySet().iterator(); + while (itBrokerTable.hasNext()) { + Entry> entry = itBrokerTable.next(); + String brokerName = entry.getKey(); + HashMap oneTable = entry.getValue(); + + HashMap cloneAddrTable = new HashMap<>(oneTable.size(), 1); + cloneAddrTable.putAll(oneTable); + + Iterator> it = cloneAddrTable.entrySet().iterator(); + while (it.hasNext()) { + Entry ee = it.next(); + String addr = ee.getValue(); + if (!this.isBrokerAddrExistInTopicRouteTable(addr)) { + it.remove(); + log.info("the broker addr[{} {}] is offline, remove it", brokerName, addr); + } + } + + if (cloneAddrTable.isEmpty()) { + itBrokerTable.remove(); + log.info("the broker[{}] name's host is offline, remove it", brokerName); + } else { + updatedTable.put(brokerName, cloneAddrTable); + } + } + + if (!updatedTable.isEmpty()) { + this.brokerAddrTable.putAll(updatedTable); + } + } finally { + this.lockNamesrv.unlock(); + } + } catch (InterruptedException e) { + log.warn("cleanOfflineBroker Exception", e); + } + } + + public void checkClientInBroker() throws MQClientException { + + for (Entry entry : this.consumerTable.entrySet()) { + Set subscriptionInner = entry.getValue().subscriptions(); + if (subscriptionInner == null || subscriptionInner.isEmpty()) { + return; + } + + for (SubscriptionData subscriptionData : subscriptionInner) { + if (ExpressionType.isTagType(subscriptionData.getExpressionType())) { + continue; + } + // may need to check one broker every cluster... + // 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, clientConfig.getMqClientApiTimeout() + ); + } catch (Exception e) { + if (e instanceof MQClientException) { + throw (MQClientException) e; + } else { + throw new MQClientException("Check client in broker error, maybe because you use " + + subscriptionData.getExpressionType() + " to filter message, but server has not been upgraded to support!" + + "This error would not affect the launch of consumer, but may has impact on message receiving if you " + + "have use the new features which are not supported by server, please check the log!", e); + } + } + } + } + } + } + + public boolean sendHeartbeatToAllBrokerWithLockV2(boolean isRebalance) { + if (this.lockHeartbeat.tryLock()) { + try { + 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. [{}]", this.clientId); + } + return false; + } + + private void persistAllConsumerOffset() { + for (Entry entry : this.consumerTable.entrySet()) { + MQConsumerInner impl = entry.getValue(); + impl.persistConsumerOffset(); + } + } + + public void adjustThreadPool() { + for (Entry entry : this.consumerTable.entrySet()) { + MQConsumerInner impl = entry.getValue(); + if (impl != null) { + try { + if (impl instanceof DefaultMQPushConsumerImpl) { + DefaultMQPushConsumerImpl dmq = (DefaultMQPushConsumerImpl) impl; + dmq.adjustThreadPool(); + } + } catch (Exception ignored) { + } + } + } + } + + public boolean updateTopicRouteInfoFromNameServer(final String topic) { + return updateTopicRouteInfoFromNameServer(topic, false, null); + } + + private boolean isBrokerAddrExistInTopicRouteTable(final String addr) { + for (Entry entry : this.topicRouteTable.entrySet()) { + TopicRouteData topicRouteData = entry.getValue(); + List bds = topicRouteData.getBrokerDatas(); + for (BrokerData bd : bds) { + if (bd.getBrokerAddrs() != null) { + boolean exist = bd.getBrokerAddrs().containsValue(addr); + if (exist) + return true; + } + } + } + + return false; + } + + public boolean sendHeartbeatToBroker(long id, String brokerName, String addr) { + return sendHeartbeatToBroker(id, brokerName, addr, true); + } + + /** + * @param id + * @param brokerName + * @param addr + * @param strictLockMode When the connection is initially established, sending a heartbeat will simultaneously trigger the onChannelActive event to acquire the lock again, causing an exception. Therefore, + * the exception that occurs when sending the heartbeat during the initial onChannelActive event can be ignored. + * @return + */ + public boolean sendHeartbeatToBroker(long id, String brokerName, String addr, boolean strictLockMode) { + 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 { + if (strictLockMode) { + 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. [{}]", this.clientId); + return false; + } + + 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; + } + + 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 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, + DefaultMQProducer defaultMQProducer) { + try { + if (this.lockNamesrv.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { + try { + TopicRouteData topicRouteData; + if (isDefault && defaultMQProducer != null) { + topicRouteData = this.mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(clientConfig.getMqClientApiTimeout()); + if (topicRouteData != null) { + for (QueueData data : topicRouteData.getQueueDatas()) { + int queueNums = Math.min(defaultMQProducer.getDefaultTopicQueueNums(), data.getReadQueueNums()); + data.setReadQueueNums(queueNums); + data.setWriteQueueNums(queueNums); + } + } + } else { + topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, clientConfig.getMqClientApiTimeout()); + } + if (topicRouteData != null) { + TopicRouteData old = this.topicRouteTable.get(topic); + boolean changed = topicRouteData.topicRouteDataChanged(old); + if (!changed) { + changed = this.isNeedUpdateTopicRouteInfo(topic); + } else { + log.info("the topic[{}] route info changed, old[{}] ,new[{}]", topic, old, topicRouteData); + } + + if (changed) { + + 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); + for (Entry entry : this.producerTable.entrySet()) { + MQProducerInner impl = entry.getValue(); + if (impl != null) { + impl.updateTopicPublishInfo(topic, publishInfo); + } + } + } + + // Update sub info + if (!consumerTable.isEmpty()) { + Set subscribeInfo = topicRouteData2TopicSubscribeInfo(topic, topicRouteData); + 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, this.clientId); + } + } 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, this.clientId); + } + } catch (InterruptedException e) { + log.warn("updateTopicRouteInfoFromNameServer Exception", e); + } + + return false; + } + + private HeartbeatData prepareHeartbeatData(boolean isWithoutSub) { + HeartbeatData heartbeatData = new HeartbeatData(); + + // clientID + heartbeatData.setClientID(this.clientId); + + // Consumer + for (Map.Entry entry : this.consumerTable.entrySet()) { + MQConsumerInner impl = entry.getValue(); + if (impl != null) { + ConsumerData consumerData = new ConsumerData(); + consumerData.setGroupName(impl.groupName()); + consumerData.setConsumeType(impl.consumeType()); + consumerData.setMessageModel(impl.messageModel()); + consumerData.setConsumeFromWhere(impl.consumeFromWhere()); + consumerData.setUnitMode(impl.isUnitMode()); + if (!isWithoutSub) { + consumerData.getSubscriptionDataSet().addAll(impl.subscriptions()); + } + heartbeatData.getConsumerDataSet().add(consumerData); + } + } + + // Producer + for (Map.Entry entry : this.producerTable.entrySet()) { + MQProducerInner impl = entry.getValue(); + if (impl != null) { + ProducerData producerData = new ProducerData(); + producerData.setGroupName(entry.getKey()); + + heartbeatData.getProducerDataSet().add(producerData); + } + } + heartbeatData.setWithoutSub(isWithoutSub); + return heartbeatData; + } + + private boolean isBrokerInNameServer(final String brokerAddr) { + for (Entry itNext : this.topicRouteTable.entrySet()) { + List brokerDatas = itNext.getValue().getBrokerDatas(); + for (BrokerData bd : brokerDatas) { + boolean contain = bd.getBrokerAddrs().containsValue(brokerAddr); + if (contain) + return true; + } + } + + return false; + } + + 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); + } + } + + if (result) { + return true; + } + + 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); + } + } + + return result; + } + + public void shutdown() { + // Consumer + if (!this.consumerTable.isEmpty()) + return; + + // AdminExt + if (!this.adminExtTable.isEmpty()) + return; + + // Producer + if (this.producerTable.size() > 1) + return; + + synchronized (this) { + switch (this.serviceState) { + case RUNNING: + this.defaultMQProducer.getDefaultMQProducerImpl().shutdown(false); + + this.serviceState = ServiceState.SHUTDOWN_ALREADY; + this.pullMessageService.shutdown(true); + this.scheduledExecutorService.shutdown(); + this.mQClientAPIImpl.shutdown(); + this.rebalanceService.shutdown(); + + MQClientManager.getInstance().removeClientFactory(this.clientId); + log.info("the client factory [{}] shutdown OK", this.clientId); + break; + case CREATE_JUST: + case SHUTDOWN_ALREADY: + default: + break; + } + } + } + + public synchronized boolean registerConsumer(final String group, final MQConsumerInner consumer) { + if (null == group || null == consumer) { + return false; + } + + MQConsumerInner prev = this.consumerTable.putIfAbsent(group, consumer); + if (prev != null) { + log.warn("the consumer group[" + group + "] exist already."); + return false; + } + + return true; + } + + public synchronized void unregisterConsumer(final String group) { + this.consumerTable.remove(group); + this.unregisterClient(null, group); + } + + private void unregisterClient(final String producerGroup, final String consumerGroup) { + 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 synchronized boolean registerProducer(final String group, final DefaultMQProducerImpl producer) { + if (null == group || null == producer) { + return false; + } + + MQProducerInner prev = this.producerTable.putIfAbsent(group, producer); + if (prev != null) { + log.warn("the producer group[{}] exist already.", group); + return false; + } + + return true; + } + + public synchronized void unregisterProducer(final String group) { + this.producerTable.remove(group); + this.unregisterClient(group, null); + } + + public boolean registerAdminExt(final String group, final MQAdminExtInner admin) { + if (null == group || null == admin) { + return false; + } + + MQAdminExtInner prev = this.adminExtTable.putIfAbsent(group, admin); + if (prev != null) { + log.warn("the admin group[{}] exist already.", group); + return false; + } + + return true; + } + + 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 boolean doRebalance() { + boolean balanced = true; + for (Map.Entry entry : this.consumerTable.entrySet()) { + MQConsumerInner impl = entry.getValue(); + if (impl != null) { + try { + if (!impl.tryRebalance()) { + balanced = false; + } + } catch (Throwable e) { + log.error("doRebalance for consumer group [{}] exception", entry.getKey(), e); + } + } + } + + return balanced; + } + + public MQProducerInner selectProducer(final String group) { + return this.producerTable.get(group); + } + + 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; + + HashMap map = this.brokerAddrTable.get(brokerName); + if (map != null && !map.isEmpty()) { + for (Map.Entry entry : map.entrySet()) { + Long id = entry.getKey(); + brokerAddr = entry.getValue(); + if (brokerAddr != null) { + found = true; + slave = MixAll.MASTER_ID != id; + break; + + } + } // end of for + } + + if (found) { + return new FindBrokerResult(brokerAddr, slave, findBrokerVersion(brokerName, brokerAddr)); + } + + return null; + } + + 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); + } + + return null; + } + + public FindBrokerResult findBrokerAddressInSubscribe( + final String brokerName, + final long brokerId, + final boolean onlyThisBroker + ) { + if (brokerName == null) { + return null; + } + String brokerAddr = null; + boolean slave = false; + boolean found = false; + + HashMap map = this.brokerAddrTable.get(brokerName); + if (map != null && !map.isEmpty()) { + brokerAddr = map.get(brokerId); + 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(); + slave = entry.getKey() != MixAll.MASTER_ID; + found = brokerAddr != null; + } + } + + if (found) { + return new FindBrokerResult(brokerAddr, slave, findBrokerVersion(brokerName, brokerAddr)); + } + + return null; + } + + 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; + } + + public List findConsumerIdList(final String topic, final String group) { + String brokerAddr = this.findBrokerAddrByTopic(topic); + if (null == brokerAddr) { + this.updateTopicRouteInfoFromNameServer(topic); + brokerAddr = this.findBrokerAddrByTopic(topic); + } + + if (null != brokerAddr) { + try { + return this.mQClientAPIImpl.getConsumerIdListByGroup(brokerAddr, group, clientConfig.getMqClientApiTimeout()); + } catch (Exception e) { + log.warn("getConsumerIdListByGroup exception, " + brokerAddr + " " + group, e); + } + } + + 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) { + List brokers = topicRouteData.getBrokerDatas(); + if (!brokers.isEmpty()) { + BrokerData bd = brokers.get(random.nextInt(brokers.size())); + return bd.selectBrokerAddr(); + } + } + + return null; + } + + public synchronized void resetOffset(String topic, String group, Map offsetTable) { + DefaultMQPushConsumerImpl consumer = null; + try { + MQConsumerInner impl = this.consumerTable.get(group); + if (impl instanceof DefaultMQPushConsumerImpl) { + consumer = (DefaultMQPushConsumerImpl) impl; + } else { + log.info("[reset-offset] consumer does not exist. group={}", group); + return; + } + consumer.suspend(); + + ConcurrentMap processQueueTable = consumer.getRebalanceImpl().getProcessQueueTable(); + for (Map.Entry entry : processQueueTable.entrySet()) { + MessageQueue mq = entry.getKey(); + if (topic.equals(mq.getTopic()) && offsetTable.containsKey(mq)) { + ProcessQueue pq = entry.getValue(); + pq.setDropped(true); + pq.clear(); + } + } + + try { + TimeUnit.SECONDS.sleep(10); + } catch (InterruptedException ignored) { + } + + Iterator iterator = processQueueTable.keySet().iterator(); + while (iterator.hasNext()) { + MessageQueue mq = iterator.next(); + Long offset = offsetTable.get(mq); + if (topic.equals(mq.getTopic()) && offset != null) { + try { + consumer.updateConsumeOffset(mq, offset); + consumer.getRebalanceImpl().removeUnnecessaryMessageQueue(mq, processQueueTable.get(mq)); + iterator.remove(); + } catch (Exception e) { + log.warn("reset offset failed. group={}, {}", group, mq, e); + } + } + } + } finally { + if (consumer != null) { + consumer.resume(); + } + } + } + + @SuppressWarnings("unchecked") + public Map getConsumerStatus(String topic, String group) { + MQConsumerInner impl = this.consumerTable.get(group); + if (impl instanceof DefaultMQPushConsumerImpl) { + DefaultMQPushConsumerImpl consumer = (DefaultMQPushConsumerImpl) impl; + return consumer.getOffsetStore().cloneOffsetTable(topic); + } else if (impl instanceof DefaultMQPullConsumerImpl) { + DefaultMQPullConsumerImpl consumer = (DefaultMQPullConsumerImpl) impl; + return consumer.getOffsetStore().cloneOffsetTable(topic); + } else { + return Collections.EMPTY_MAP; + } + } + + public TopicRouteData getAnExistTopicRouteData(final String topic) { + return this.topicRouteTable.get(topic); + } + + public MQClientAPIImpl getMQClientAPIImpl() { + return mQClientAPIImpl; + } + + public MQAdminImpl getMQAdminImpl() { + return mQAdminImpl; + } + + public long getBootTimestamp() { + return bootTimestamp; + } + + public ScheduledExecutorService getScheduledExecutorService() { + return scheduledExecutorService; + } + + public PullMessageService getPullMessageService() { + return pullMessageService; + } + + public DefaultMQProducer getDefaultMQProducer() { + return defaultMQProducer; + } + + public ConcurrentMap getTopicRouteTable() { + return topicRouteTable; + } + + public ConsumeMessageDirectlyResult consumeMessageDirectly(final MessageExt msg, + final String consumerGroup, + final String brokerName) { + MQConsumerInner mqConsumerInner = this.consumerTable.get(consumerGroup); + if (mqConsumerInner instanceof DefaultMQPushConsumerImpl) { + DefaultMQPushConsumerImpl consumer = (DefaultMQPushConsumerImpl) mqConsumerInner; + + return consumer.getConsumeMessageService().consumeMessageDirectly(msg, brokerName); + } + + return null; + } + + public ConsumerRunningInfo consumerRunningInfo(final String consumerGroup) { + MQConsumerInner mqConsumerInner = this.consumerTable.get(consumerGroup); + if (mqConsumerInner == null) { + return null; + } + + ConsumerRunningInfo consumerRunningInfo = mqConsumerInner.consumerRunningInfo(); + + List nsList = this.mQClientAPIImpl.getRemotingClient().getNameServerAddressList(); + + StringBuilder strBuilder = new StringBuilder(); + if (nsList != null) { + for (String addr : nsList) { + strBuilder.append(addr).append(";"); + } + } + + String nsAddr = strBuilder.toString(); + consumerRunningInfo.getProperties().put(ConsumerRunningInfo.PROP_NAMESERVER_ADDR, nsAddr); + consumerRunningInfo.getProperties().put(ConsumerRunningInfo.PROP_CONSUME_TYPE, mqConsumerInner.consumeType().name()); + consumerRunningInfo.getProperties().put(ConsumerRunningInfo.PROP_CLIENT_VERSION, + MQVersion.getVersionDesc(MQVersion.CURRENT_VERSION)); + + return consumerRunningInfo; + } + + private void resetBrokerAddrHeartbeatFingerprintMap() { + brokerAddrHeartbeatFingerprintTable.clear(); + } + + public ConsumerStatsManager getConsumerStatsManager() { + return consumerStatsManager; + } + + public NettyClientConfig getNettyClientConfig() { + return nettyClientConfig; + } + + public ClientConfig getClientConfig() { + return clientConfig; + } + + public ConcurrentMap getProducerTable() { + return producerTable; + } + + public ConcurrentMap getConsumerTable() { + return consumerTable; + } + + 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 0000000..9d489f8 --- /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 0000000..9089503 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/mqclient/MQClientAPIExt.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.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.NotifyResult; +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.ObjectCreator; +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.RemotingClient; +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.RecallMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.RecallMessageResponseHeader; +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 + ) { + this(clientConfig, nettyClientConfig, clientRemotingProcessor, rpcHook, null); + } + + public MQClientAPIExt( + ClientConfig clientConfig, + NettyClientConfig nettyClientConfig, + ClientRemotingProcessor clientRemotingProcessor, + RPCHook rpcHook, + ObjectCreator remotingClientCreator + ) { + super(nettyClientConfig, clientRemotingProcessor, rpcHook, clientConfig, null, remotingClientCreator); + 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 updateConsumerOffsetAsync( + String brokerAddr, + UpdateConsumerOffsetRequestHeader header, + long timeoutMillis + ) { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_CONSUMER_OFFSET, header); + CompletableFuture future = new CompletableFuture<>(); + invoke(brokerAddr, request, timeoutMillis).whenComplete((response, t) -> { + if (t != null) { + log.error("updateConsumerOffsetAsync failed, brokerAddr={}, requestHeader={}", brokerAddr, header, t); + future.completeExceptionally(t); + return; + } + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + future.complete(null); + } + case ResponseCode.SYSTEM_ERROR: + case ResponseCode.SUBSCRIPTION_GROUP_NOT_EXIST: + case ResponseCode.TOPIC_NOT_EXIST: { + future.completeExceptionally(new MQBrokerException(response.getCode(), response.getRemark())); + } + } + }); + 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) { + return notificationWithPollingStats(brokerAddr, requestHeader, timeoutMillis).thenApply(NotifyResult::isHasMsg); + } + + public CompletableFuture notificationWithPollingStats(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); + NotifyResult notifyResult = new NotifyResult(); + notifyResult.setHasMsg(responseHeader.isHasMsg()); + notifyResult.setPollingFull(responseHeader.isPollingFull()); + future0.complete(notifyResult); + } catch (Throwable t) { + future0.completeExceptionally(t); + } + } else { + future0.completeExceptionally(new MQBrokerException(response.getCode(), response.getRemark())); + } + return future0; + }); + } + + public CompletableFuture recallMessageAsync(String brokerAddr, + RecallMessageRequestHeader requestHeader, long timeoutMillis) { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.RECALL_MESSAGE, requestHeader); + return this.getRemotingClient().invoke(brokerAddr, request, timeoutMillis).thenCompose(response -> { + CompletableFuture future = new CompletableFuture<>(); + if (ResponseCode.SUCCESS == response.getCode()) { + try { + RecallMessageResponseHeader responseHeader = + response.decodeCommandCustomHeader(RecallMessageResponseHeader.class); + future.complete(responseHeader.getMsgId()); + } catch (Throwable t) { + future.completeExceptionally(t); + } + } else { + future.completeExceptionally(new MQBrokerException(response.getCode(), response.getRemark(), brokerAddr)); + } + return future; + }); + } + + 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 0000000..d85dcc7 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/mqclient/MQClientAPIFactory.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.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.AsyncShutdownHelper; +import org.apache.rocketmq.common.ObjectCreator; +import org.apache.rocketmq.common.utils.StartAndShutdown; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.RemotingClient; +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; + private final ObjectCreator remotingClientCreator; + + public MQClientAPIFactory( + NameserverAccessConfig nameserverAccessConfig, + String namePrefix, + int clientNum, + ClientRemotingProcessor clientRemotingProcessor, + RPCHook rpcHook, + ScheduledExecutorService scheduledExecutorService + ) { + this(nameserverAccessConfig, namePrefix, clientNum, clientRemotingProcessor, rpcHook, scheduledExecutorService, null); + } + + public MQClientAPIFactory( + NameserverAccessConfig nameserverAccessConfig, + String namePrefix, + int clientNum, + ClientRemotingProcessor clientRemotingProcessor, + RPCHook rpcHook, + ScheduledExecutorService scheduledExecutorService, + ObjectCreator remotingClientCreator + ) { + this.nameserverAccessConfig = nameserverAccessConfig; + this.namePrefix = namePrefix; + this.clientNum = clientNum; + this.clientRemotingProcessor = clientRemotingProcessor; + this.rpcHook = rpcHook; + this.scheduledExecutorService = scheduledExecutorService; + this.remotingClientCreator = remotingClientCreator; + + 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 { + AsyncShutdownHelper helper = new AsyncShutdownHelper(); + for (int i = 0; i < this.clientNum; i++) { + helper.addTarget(clients[i]); + } + helper.shutdown().await(Integer.MAX_VALUE, TimeUnit.SECONDS); + } + + 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, + remotingClientCreator + ); + + 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 new file mode 100644 index 0000000..4aa6058 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/producer/DefaultMQProducerImpl.java @@ -0,0 +1,1888 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.producer; + +import java.io.IOException; +import java.net.UnknownHostException; +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.Optional; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +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.commons.lang3.StringUtils; +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.latency.Resolver; +import org.apache.rocketmq.client.latency.ServiceDetector; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +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.help.FAQUrl; +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.producer.RecallMessageHandle; +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.exception.RemotingConnectException; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; +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.RecallMessageRequestHeader; +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 = 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<>(); + 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 MQFaultStrategy mqFaultStrategy; + private ExecutorService asyncSenderExecutor; + + // backpressure related + private Semaphore semaphoreAsyncSendNum; + private Semaphore semaphoreAsyncSendSize; + + public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer) { + this(defaultMQProducer, null); + } + + 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.empty(); + } + 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 int getSemaphoreAsyncSendNumAvailablePermits() { + return semaphoreAsyncSendNum == null ? 0 : semaphoreAsyncSendNum.availablePermits(); + } + + public int getSemaphoreAsyncSendSizeAvailablePermits() { + return semaphoreAsyncSendSize == null ? 0 : semaphoreAsyncSendSize.availablePermits(); + } + + public void initTransactionEnv() { + TransactionMQProducer producer = (TransactionMQProducer) this.defaultMQProducer; + 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() { + if (this.checkExecutor != null) { + this.checkExecutor.shutdown(); + } + } + + public void registerSendMessageHook(final SendMessageHook hook) { + this.sendMessageHookList.add(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); + } + + public void start(final boolean startFactory) throws MQClientException { + switch (this.serviceState) { + case CREATE_JUST: + this.serviceState = ServiceState.START_FAILED; + + this.checkConfig(); + + if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) { + this.defaultMQProducer.changeInstanceNameToPID(); + } + + this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook); + + defaultMQProducer.initProduceAccumulator(); + + boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this); + if (!registerOK) { + this.serviceState = ServiceState.CREATE_JUST; + throw new MQClientException("The producer group[" + this.defaultMQProducer.getProducerGroup() + + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL), + null); + } + + 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; + break; + case RUNNING: + case START_FAILED: + case SHUTDOWN_ALREADY: + throw new MQClientException("The producer service state not OK, maybe started once, " + + this.serviceState + + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK), + null); + default: + break; + } + + this.mQClientFactory.sendHeartbeatToAllBrokerWithLock(); + + RequestFutureHolder.getInstance().startScheduledTask(this); + + } + + private void checkConfig() throws MQClientException { + Validators.checkGroup(this.defaultMQProducer.getProducerGroup()); + + 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); + } + } + + public void shutdown() { + this.shutdown(true); + } + + public void shutdown(final boolean shutdownFactory) { + switch (this.serviceState) { + case CREATE_JUST: + 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; + case SHUTDOWN_ALREADY: + break; + default: + break; + } + } + + @Override + public Set getPublishTopicList() { + return new HashSet<>(this.topicPublishInfoTable.keySet()); + } + + @Override + public boolean isPublishTopicNeedUpdate(String topic) { + TopicPublishInfo prev = this.topicPublishInfoTable.get(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; + return producer.getTransactionCheckListener(); + } + + 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) { + Runnable request = new Runnable() { + private final String brokerAddr = addr; + private final MessageExt message = msg; + private final CheckTransactionStateRequestHeader checkRequestHeader = header; + private final String group = DefaultMQProducerImpl.this.defaultMQProducer.getProducerGroup(); + + @Override + public void run() { + TransactionCheckListener transactionCheckListener = DefaultMQProducerImpl.this.checkListener(); + TransactionListener transactionListener = getCheckListener(); + if (transactionCheckListener != null || transactionListener != null) { + LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW; + Throwable exception = null; + try { + 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; + } + + this.processTransactionState( + checkRequestHeader.getTopic(), + localTransactionState, + group, + exception); + } else { + log.warn("CheckTransactionState, pick transactionCheckListener by group[{}] failed", group); + } + } + + private void processTransactionState( + final String topic, + final LocalTransactionState localTransactionState, + final String producerGroup, + final Throwable exception) { + final EndTransactionRequestHeader thisHeader = new EndTransactionRequestHeader(); + thisHeader.setTopic(topic); + thisHeader.setCommitLogOffset(checkRequestHeader.getCommitLogOffset()); + 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) { + uniqueKey = message.getMsgId(); + } + thisHeader.setMsgId(uniqueKey); + thisHeader.setTransactionId(checkRequestHeader.getTransactionId()); + switch (localTransactionState) { + case COMMIT_MESSAGE: + thisHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_COMMIT_TYPE); + break; + case ROLLBACK_MESSAGE: + thisHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE); + log.warn("when broker check, client rollback this transaction, {}", thisHeader); + break; + case UNKNOW: + thisHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_NOT_TYPE); + log.warn("when broker check, client does not know this transaction state, {}", thisHeader); + break; + default: + break; + } + + String remark = null; + if (exception != null) { + remark = "checkLocalTransactionState Exception: " + UtilAll.exceptionSimpleDesc(exception); + } + doExecuteEndTransactionHook(msg, uniqueKey, brokerAddr, localTransactionState, true); + + try { + DefaultMQProducerImpl.this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr, thisHeader, remark, + 3000); + } catch (Exception e) { + log.error("endTransactionOneway exception", e); + } + } + }; + + this.checkExecutor.submit(request); + } + + @Override + public 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); + } + } + } + + @Override + public boolean isUnitMode() { + return this.defaultMQProducer.isUnitMode(); + } + + 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, int topicSysFlag) throws MQClientException { + this.makeSureStateOK(); + Validators.checkTopic(newTopic); + Validators.isSystemTopic(newTopic); + + this.mQClientFactory.getMQAdminImpl().createTopic(key, newTopic, queueNum, topicSysFlag, null); + } + + private void makeSureStateOK() throws MQClientException { + if (this.serviceState != ServiceState.RUNNING) { + throw new MQClientException("The producer service state not OK, " + + this.serviceState + + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK), + null); + } + } + + public List fetchPublishMessageQueues(String topic) throws MQClientException { + this.makeSureStateOK(); + return this.mQClientFactory.getMQAdminImpl().fetchPublishMessageQueues(topic); + } + + public long searchOffset(MessageQueue mq, long timestamp) throws MQClientException { + this.makeSureStateOK(); + return this.mQClientFactory.getMQAdminImpl().searchOffset(mq, timestamp); + } + + public long maxOffset(MessageQueue mq) throws MQClientException { + this.makeSureStateOK(); + return this.mQClientFactory.getMQAdminImpl().maxOffset(mq); + } + + public long minOffset(MessageQueue mq) throws MQClientException { + this.makeSureStateOK(); + return this.mQClientFactory.getMQAdminImpl().minOffset(mq); + } + + public long earliestMsgStoreTime(MessageQueue mq) throws MQClientException { + this.makeSureStateOK(); + return this.mQClientFactory.getMQAdminImpl().earliestMsgStoreTime(mq); + } + + public MessageExt viewMessage(String topic, + String msgId) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + this.makeSureStateOK(); + + return this.mQClientFactory.getMQAdminImpl().viewMessage(topic, msgId); + } + + public QueryResult queryMessage(String topic, String key, int maxNum, long begin, long end) + throws MQClientException, InterruptedException { + this.makeSureStateOK(); + return this.mQClientFactory.getMQAdminImpl().queryMessage(topic, key, maxNum, begin, end); + } + + public MessageExt queryMessageByUniqKey(String topic, String uniqKey) + throws MQClientException, InterruptedException { + this.makeSureStateOK(); + return this.mQClientFactory.getMQAdminImpl().queryMessageByUniqKey(topic, uniqKey); + } + + /** + * DEFAULT ASYNC ------------------------------------------------------- + */ + public void send(Message msg, + SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException { + send(msg, sendCallback, this.defaultMQProducer.getSendMsgTimeout()); + } + + /** + * @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 isSemaphoreAsyncSizeAcquired = false; + public boolean isSemaphoreAsyncNumAcquired = false; + public int msgLen; + private final SendCallback sendCallback; + + public BackpressureSendCallBack(final SendCallback sendCallback) { + this.sendCallback = sendCallback; + } + + @Override + public void onSuccess(SendResult sendResult) { + semaphoreProcessor(); + sendCallback.onSuccess(sendResult); + } + + @Override + public void onException(Throwable e) { + semaphoreProcessor(); + sendCallback.onException(e); + } + + public void semaphoreProcessor() { + if (isSemaphoreAsyncSizeAcquired) { + defaultMQProducer.acquireBackPressureForAsyncSendSizeLock(); + semaphoreAsyncSendSize.release(msgLen); + defaultMQProducer.releaseBackPressureForAsyncSendSizeLock(); + } + if (isSemaphoreAsyncNumAcquired) { + defaultMQProducer.acquireBackPressureForAsyncSendNumLock(); + semaphoreAsyncSendNum.release(); + defaultMQProducer.releaseBackPressureForAsyncSendNumLock(); + } + } + + public void semaphoreAsyncAdjust(int semaphoreAsyncNum, int semaphoreAsyncSize) throws InterruptedException { + defaultMQProducer.acquireBackPressureForAsyncSendNumLock(); + if (semaphoreAsyncNum > 0) { + semaphoreAsyncSendNum.release(semaphoreAsyncNum); + } else { + semaphoreAsyncSendNum.acquire(- semaphoreAsyncNum); + } + defaultMQProducer.setBackPressureForAsyncSendNumInsideAdjust(defaultMQProducer.getBackPressureForAsyncSendNum() + + semaphoreAsyncNum); + defaultMQProducer.releaseBackPressureForAsyncSendNumLock(); + + defaultMQProducer.acquireBackPressureForAsyncSendSizeLock(); + if (semaphoreAsyncSize > 0) { + semaphoreAsyncSendSize.release(semaphoreAsyncSize); + } else { + semaphoreAsyncSendSize.acquire(- semaphoreAsyncSize); + } + defaultMQProducer.setBackPressureForAsyncSendSizeInsideAdjust(defaultMQProducer.getBackPressureForAsyncSendSize() + + semaphoreAsyncSize); + defaultMQProducer.releaseBackPressureForAsyncSendSizeLock(); + } + } + + 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 isSemaphoreAsyncNumAcquired = false; + boolean isSemaphoreAsyncSizeAcquired = false; + int msgLen = msg.getBody() == null ? 1 : msg.getBody().length; + sendCallback.msgLen = msgLen; + + try { + if (isEnableBackpressureForAsyncMode) { + defaultMQProducer.acquireBackPressureForAsyncSendNumLock(); + long costTime = System.currentTimeMillis() - beginStartTime; + + isSemaphoreAsyncNumAcquired = timeout - costTime > 0 + && semaphoreAsyncSendNum.tryAcquire(timeout - costTime, TimeUnit.MILLISECONDS); + sendCallback.isSemaphoreAsyncNumAcquired = isSemaphoreAsyncNumAcquired; + defaultMQProducer.releaseBackPressureForAsyncSendNumLock(); + if (!isSemaphoreAsyncNumAcquired) { + sendCallback.onException( + new RemotingTooMuchRequestException("send message tryAcquire semaphoreAsyncNum timeout")); + return; + } + + defaultMQProducer.acquireBackPressureForAsyncSendSizeLock(); + costTime = System.currentTimeMillis() - beginStartTime; + + isSemaphoreAsyncSizeAcquired = timeout - costTime > 0 + && semaphoreAsyncSendSize.tryAcquire(msgLen, timeout - costTime, TimeUnit.MILLISECONDS); + sendCallback.isSemaphoreAsyncSizeAcquired = isSemaphoreAsyncSizeAcquired; + defaultMQProducer.releaseBackPressureForAsyncSendSizeLock(); + if (!isSemaphoreAsyncSizeAcquired) { + sendCallback.onException( + new RemotingTooMuchRequestException("send message tryAcquire semaphoreAsyncSize timeout")); + return; + } + } + + executor.submit(runnable); + } catch (RejectedExecutionException e) { + if (isEnableBackpressureForAsyncMode) { + runnable.run(); + } else { + throw new MQClientException("executor rejected ", e); + } + } + } + + 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 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( + Message msg, + final CommunicationMode communicationMode, + final SendCallback sendCallback, + final long timeout + ) 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(); + 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(); + 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; + } + long curTimeout = timeout - costTime; + // Get the maximum timeout allowed per request + long maxSendTimeoutPerRequest = defaultMQProducer.getSendMsgMaxTimeoutPerRequest(); + // Determine if retries are still possible + boolean canRetryAgain = times + 1 < timesTotal; + // If retries are possible, and the current timeout exceeds the max allowed timeout, set the current timeout to the max allowed + if (maxSendTimeoutPerRequest > -1 && canRetryAgain && curTimeout > maxSendTimeoutPerRequest) { + curTimeout = maxSendTimeoutPerRequest; + } + sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, curTimeout); + endTimestamp = System.currentTimeMillis(); + this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false, true); + switch (communicationMode) { + case ASYNC: + return null; + case ONEWAY: + return null; + case SYNC: + if (sendResult.getSendStatus() != SendStatus.SEND_OK) { + if (this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) { + continue; + } + } + + return sendResult; + default: + break; + } + } catch (MQClientException e) { + endTimestamp = System.currentTimeMillis(); + this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false, true); + log.warn("sendKernelImpl exception, resend at once, InvokeID: {}, RT: {}ms, Broker: {}", invokeID, endTimestamp - beginTimestampPrev, mq, e); + log.warn(msg.toString()); + exception = e; + continue; + } catch (RemotingException e) { + endTimestamp = System.currentTimeMillis(); + 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: {}, RT: {}ms, Broker: {}", 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, false); + log.warn("sendKernelImpl exception, resend at once, InvokeID: {}, RT: {}ms, Broker: {}", invokeID, endTimestamp - beginTimestampPrev, mq, e); + if (log.isDebugEnabled()) { + log.debug(msg.toString()); + } + exception = e; + if (this.defaultMQProducer.getRetryResponseCodes().contains(e.getResponseCode())) { + continue; + } else { + if (sendResult != null) { + return sendResult; + } + + throw e; + } + } catch (InterruptedException e) { + endTimestamp = System.currentTimeMillis(); + this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false, true); + log.warn("sendKernelImpl exception, throw exception, InvokeID: {}, RT: {}ms, Broker: {}", invokeID, endTimestamp - beginTimestampPrev, mq, e); + if (log.isDebugEnabled()) { + log.debug(msg.toString()); + } + throw e; + } + } else { + break; + } + } + + 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, + msg.getTopic(), + Arrays.toString(brokersSent)); + + 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) { + mqClientException.setResponseCode(ClientErrorCode.CONNECT_BROKER_EXCEPTION); + } else if (exception instanceof RemotingTimeoutException) { + mqClientException.setResponseCode(ClientErrorCode.ACCESS_BROKER_TIMEOUT); + } else if (exception instanceof MQClientException) { + mqClientException.setResponseCode(ClientErrorCode.BROKER_NOT_EXIST_EXCEPTION); + } + + throw mqClientException; + } + + validateNameServerSetting(); + + 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); + } + + private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) { + TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic); + if (null == topicPublishInfo || !topicPublishInfo.ok()) { + this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo()); + this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic); + topicPublishInfo = this.topicPublishInfoTable.get(topic); + } + + if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) { + return topicPublishInfo; + } else { + this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer); + topicPublishInfo = this.topicPublishInfoTable.get(topic); + return topicPublishInfo; + } + } + + private SendResult sendKernelImpl(final Message msg, + final MessageQueue mq, + final CommunicationMode communicationMode, + final SendCallback sendCallback, + final TopicPublishInfo topicPublishInfo, + final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + long beginStartTime = System.currentTimeMillis(); + String brokerName = this.mQClientFactory.getBrokerNameFromMessageQueue(mq); + String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(brokerName); + if (null == brokerAddr) { + tryToFindTopicPublishInfo(mq.getTopic()); + brokerName = this.mQClientFactory.getBrokerNameFromMessageQueue(mq); + brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(brokerName); + } + + SendMessageContext context = null; + if (brokerAddr != null) { + brokerAddr = MixAll.brokerVIPChannel(this.defaultMQProducer.isSendMessageWithVIPChannel(), brokerAddr); + + byte[] prevBody = msg.getBody(); + try { + //for MessageBatch,ID has been set in the generating process + if (!(msg instanceof MessageBatch)) { + 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 |= this.defaultMQProducer.getCompressType().getCompressionFlag(); + msgBodyCompressed = true; + } + + final String tranMsg = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED); + if (Boolean.parseBoolean(tranMsg)) { + sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE; + } + + if (hasCheckForbiddenHook()) { + CheckForbiddenContext checkForbiddenContext = new CheckForbiddenContext(); + checkForbiddenContext.setNameSrvAddr(this.defaultMQProducer.getNamesrvAddr()); + checkForbiddenContext.setGroup(this.defaultMQProducer.getProducerGroup()); + checkForbiddenContext.setCommunicationMode(communicationMode); + checkForbiddenContext.setBrokerAddr(brokerAddr); + checkForbiddenContext.setMessage(msg); + checkForbiddenContext.setMq(mq); + checkForbiddenContext.setUnitMode(this.isUnitMode()); + this.executeCheckForbiddenHook(checkForbiddenContext); + } + + if (this.hasSendMessageHook()) { + context = new SendMessageContext(); + context.setProducer(this); + context.setProducerGroup(this.defaultMQProducer.getProducerGroup()); + context.setCommunicationMode(communicationMode); + context.setBornHost(this.defaultMQProducer.getClientIP()); + 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); + } + + if (msg.getProperty("__STARTDELIVERTIME") != null || msg.getProperty(MessageConst.PROPERTY_DELAY_TIME_LEVEL) != null) { + context.setMsgType(MessageType.Delay_Msg); + } + this.executeSendMessageHookBefore(context); + } + + SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); + requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup()); + requestHeader.setTopic(msg.getTopic()); + requestHeader.setDefaultTopic(this.defaultMQProducer.getCreateTopicKey()); + requestHeader.setDefaultTopicQueueNums(this.defaultMQProducer.getDefaultTopicQueueNums()); + requestHeader.setQueueId(mq.getQueueId()); + requestHeader.setSysFlag(sysFlag); + requestHeader.setBornTimestamp(System.currentTimeMillis()); + requestHeader.setFlag(msg.getFlag()); + requestHeader.setProperties(MessageDecoder.messageProperties2String(msg.getProperties())); + 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) { + requestHeader.setReconsumeTimes(Integer.valueOf(reconsumeTimes)); + MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_RECONSUME_TIME); + } + + String maxReconsumeTimes = MessageAccessor.getMaxReconsumeTimes(msg); + if (maxReconsumeTimes != null) { + requestHeader.setMaxReconsumeTimes(Integer.valueOf(maxReconsumeTimes)); + MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_MAX_RECONSUME_TIMES); + } + } + + 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 compressed 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, + brokerName, + tmpMessage, + requestHeader, + timeout - costTimeAsync, + communicationMode, + sendCallback, + topicPublishInfo, + this.mQClientFactory, + this.defaultMQProducer.getRetryTimesWhenSendAsyncFailed(), + context, + this); + 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, + brokerName, + msg, + requestHeader, + timeout - costTimeSync, + communicationMode, + context, + this); + break; + default: + assert false; + break; + } + + if (this.hasSendMessageHook()) { + context.setSendResult(sendResult); + this.executeSendMessageHookAfter(context); + } + + return sendResult; + } catch (RemotingException | InterruptedException | MQBrokerException e) { + if (this.hasSendMessageHook()) { + context.setException(e); + this.executeSendMessageHookAfter(context); + } + throw e; + } finally { + msg.setBody(prevBody); + msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), this.defaultMQProducer.getNamespace())); + } + } + + 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 does not support compressing right now + return false; + } + byte[] body = msg.getBody(); + if (body != null) { + if (body.length >= this.defaultMQProducer.getCompressMsgBodyOverHowmuch()) { + try { + byte[] data = this.defaultMQProducer.getCompressor().compress(body, this.defaultMQProducer.getCompressLevel()); + if (data != null) { + msg.setBody(data); + return true; + } + } catch (IOException e) { + log.error("tryToCompressMessage exception", e); + if (log.isDebugEnabled()) { + log.debug(msg.toString()); + } + } + } + } + + return false; + } + + public boolean hasCheckForbiddenHook() { + return !checkForbiddenHookList.isEmpty(); + } + + public void executeCheckForbiddenHook(final CheckForbiddenContext context) throws MQClientException { + if (hasCheckForbiddenHook()) { + for (CheckForbiddenHook hook : checkForbiddenHookList) { + hook.checkForbidden(context); + } + } + } + + public boolean hasSendMessageHook() { + return !this.sendMessageHookList.isEmpty(); + } + + public void executeSendMessageHookBefore(final SendMessageContext context) { + if (!this.sendMessageHookList.isEmpty()) { + for (SendMessageHook hook : this.sendMessageHookList) { + try { + hook.sendMessageBefore(context); + } catch (Throwable e) { + log.warn("failed to executeSendMessageHookBefore", e); + } + } + } + } + + public void executeSendMessageHookAfter(final SendMessageContext context) { + if (!this.sendMessageHookList.isEmpty()) { + for (SendMessageHook hook : this.sendMessageHookList) { + try { + hook.sendMessageAfter(context); + } catch (Throwable e) { + log.warn("failed to executeSendMessageHookAfter", e); + } + } + } + } + + 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 ------------------------------------------------------- + */ + public void sendOneway(Message msg) throws MQClientException, RemotingException, InterruptedException { + try { + this.sendDefaultImpl(msg, CommunicationMode.ONEWAY, null, this.defaultMQProducer.getSendMsgTimeout()); + } catch (MQBrokerException e) { + throw new MQClientException("unknown exception", e); + } + } + + /** + * KERNEL SYNC ------------------------------------------------------- + */ + public SendResult send(Message msg, MessageQueue mq) + throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + return send(msg, mq, this.defaultMQProducer.getSendMsgTimeout()); + } + + 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); + + if (!msg.getTopic().equals(mq.getTopic())) { + 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); + } + + /** + * KERNEL ASYNC ------------------------------------------------------- + */ + public void send(Message msg, MessageQueue mq, SendCallback sendCallback) + throws MQClientException, RemotingException, InterruptedException { + send(msg, mq, sendCallback, this.defaultMQProducer.getSendMsgTimeout()); + } + + /** + * @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 { + 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("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); + } + } + + }; + + executeAsyncMessageSend(runnable, msg, newCallBack, timeout, beginStartTime); + } + + /** + * KERNEL ONEWAY ------------------------------------------------------- + */ + public void sendOneway(Message msg, + MessageQueue mq) throws MQClientException, RemotingException, InterruptedException { + this.makeSureStateOK(); + Validators.checkMessage(msg, this.defaultMQProducer); + + try { + this.sendKernelImpl(msg, mq, CommunicationMode.ONEWAY, null, null, this.defaultMQProducer.getSendMsgTimeout()); + } catch (MQBrokerException e) { + throw new MQClientException("unknown exception", e); + } + } + + /** + * SELECT SYNC ------------------------------------------------------- + */ + public SendResult send(Message msg, MessageQueueSelector selector, Object arg) + throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + return send(msg, selector, arg, this.defaultMQProducer.getSendMsgTimeout()); + } + + public SendResult send(Message msg, MessageQueueSelector selector, Object arg, long timeout) + throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + return this.sendSelectImpl(msg, selector, arg, CommunicationMode.SYNC, null, timeout); + } + + private SendResult sendSelectImpl( + Message msg, + MessageQueueSelector selector, + Object arg, + 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); + + 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 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); + } + + /** + * SELECT ASYNC ------------------------------------------------------- + */ + public void send(Message msg, MessageQueueSelector selector, Object arg, SendCallback sendCallback) + throws MQClientException, RemotingException, InterruptedException { + send(msg, selector, arg, sendCallback, this.defaultMQProducer.getSendMsgTimeout()); + } + + /** + * 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 { + 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); + } + + /** + * SELECT ONEWAY ------------------------------------------------------- + */ + public void sendOneway(Message msg, MessageQueueSelector selector, Object arg) + throws MQClientException, RemotingException, InterruptedException { + try { + this.sendSelectImpl(msg, selector, arg, CommunicationMode.ONEWAY, null, this.defaultMQProducer.getSendMsgTimeout()); + } catch (MQBrokerException e) { + throw new MQClientException("unknown exception", e); + } + } + + public TransactionSendResult sendMessageInTransaction(final Message msg, + final TransactionListener localTransactionListener, final Object arg) + throws MQClientException { + 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; + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true"); + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup()); + try { + sendResult = this.send(msg); + } catch (Exception e) { + throw new MQClientException("send message Exception", e); + } + + LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW; + Throwable localException = null; + switch (sendResult.getSendStatus()) { + case SEND_OK: { + try { + if (sendResult.getTransactionId() != null) { + msg.putUserProperty("__transactionId__", sendResult.getTransactionId()); + } + 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: {} messageTopic: {} transactionId: {} tag: {} key: {}", + localTransactionState, msg.getTopic(), msg.getTransactionId(), msg.getTags(), msg.getKeys()); + } + } catch (Throwable e) { + log.error("executeLocalTransactionBranch exception, messageTopic: {} transactionId: {} tag: {} key: {}", + msg.getTopic(), msg.getTransactionId(), msg.getTags(), msg.getKeys(), e); + localException = e; + } + } + break; + case FLUSH_DISK_TIMEOUT: + case FLUSH_SLAVE_TIMEOUT: + case SLAVE_NOT_AVAILABLE: + localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE; + break; + default: + break; + } + + try { + this.endTransaction(msg, sendResult, localTransactionState, localException); + } catch (Exception e) { + log.warn("local transaction execute " + localTransactionState + ", but end broker transaction failed", e); + } + + TransactionSendResult transactionSendResult = new TransactionSendResult(); + transactionSendResult.setSendStatus(sendResult.getSendStatus()); + transactionSendResult.setMessageQueue(sendResult.getMessageQueue()); + transactionSendResult.setMsgId(sendResult.getMsgId()); + transactionSendResult.setQueueOffset(sendResult.getQueueOffset()); + transactionSendResult.setTransactionId(sendResult.getTransactionId()); + transactionSendResult.setLocalTransactionState(localTransactionState); + return transactionSendResult; + } + + /** + * DEFAULT SYNC ------------------------------------------------------- + */ + public SendResult send( + Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + return send(msg, this.defaultMQProducer.getSendMsgTimeout()); + } + + public void endTransaction( + final Message msg, + final SendResult sendResult, + final LocalTransactionState localTransactionState, + final Throwable localException) throws RemotingException, MQBrokerException, InterruptedException, UnknownHostException { + final MessageId id; + if (sendResult.getOffsetMsgId() != null) { + id = MessageDecoder.decodeMessageId(sendResult.getOffsetMsgId()); + } else { + id = MessageDecoder.decodeMessageId(sendResult.getMsgId()); + } + String transactionId = sendResult.getTransactionId(); + final String destBrokerName = this.mQClientFactory.getBrokerNameFromMessageQueue(defaultMQProducer.queueWithNamespace(sendResult.getMessageQueue())); + final String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(destBrokerName); + EndTransactionRequestHeader requestHeader = new EndTransactionRequestHeader(); + requestHeader.setTopic(msg.getTopic()); + requestHeader.setTransactionId(transactionId); + requestHeader.setCommitLogOffset(id.getOffset()); + requestHeader.setBrokerName(destBrokerName); + switch (localTransactionState) { + case COMMIT_MESSAGE: + requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_COMMIT_TYPE); + break; + case ROLLBACK_MESSAGE: + requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE); + break; + case UNKNOW: + requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_NOT_TYPE); + break; + default: + break; + } + + doExecuteEndTransactionHook(msg, sendResult.getMsgId(), brokerAddr, localTransactionState, false); + requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup()); + requestHeader.setTranStateTableOffset(sendResult.getQueueOffset()); + requestHeader.setMsgId(sendResult.getMsgId()); + String remark = localException != null ? ("executeLocalTransactionBranch exception: " + localException.toString()) : null; + this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr, requestHeader, remark, + this.defaultMQProducer.getSendMsgTimeout()); + } + + public String recallMessage( + String topic, + String recallHandle) throws RemotingException, MQClientException, MQBrokerException, InterruptedException { + makeSureStateOK(); + Validators.checkTopic(topic); + if (NamespaceUtil.isRetryTopic(topic) || NamespaceUtil.isDLQTopic(topic)) { + throw new MQClientException("topic is not supported", null); + } + RecallMessageHandle.HandleV1 handleEntity; + try { + handleEntity = (RecallMessageHandle.HandleV1) RecallMessageHandle.decodeHandle(recallHandle); + } catch (Exception e) { + throw new MQClientException(e.getMessage(), null); + } + + tryToFindTopicPublishInfo(topic); + String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(handleEntity.getBrokerName()); + brokerAddr = StringUtils.isNotEmpty(brokerAddr) ? + // find another address to support multi proxy endpoints, + // may cause failure request in proxy-less mode when the broker is temporarily unavailable + brokerAddr : this.mQClientFactory.findBrokerAddrByTopic(topic); + if (StringUtils.isEmpty(brokerAddr)) { + log.warn("can't find broker service address. {}", handleEntity.getBrokerName()); + throw new MQClientException("The broker service address not found", null); + } + RecallMessageRequestHeader requestHeader = new RecallMessageRequestHeader(); + requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup()); + requestHeader.setTopic(topic); + requestHeader.setRecallHandle(recallHandle); + requestHeader.setBrokerName(handleEntity.getBrokerName()); + return this.mQClientFactory.getMQClientAPIImpl().recallMessage(brokerAddr, + requestHeader, this.defaultMQProducer.getSendMsgTimeout()); + } + + 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); + requestResponseFuture.acquireCountDownLatch(); + } + + @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); + requestResponseFuture.acquireCountDownLatch(); + } + + @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); + requestResponseFuture.acquireCountDownLatch(); + } + + @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 ServiceState getServiceState() { + return serviceState; + } + + public void setServiceState(ServiceState serviceState) { + this.serviceState = serviceState; + } + + public long[] getNotAvailableDuration() { + return this.mqFaultStrategy.getNotAvailableDuration(); + } + + public void setNotAvailableDuration(final long[] notAvailableDuration) { + this.mqFaultStrategy.setNotAvailableDuration(notAvailableDuration); + } + + public long[] getLatencyMax() { + return this.mqFaultStrategy.getLatencyMax(); + } + + public void setLatencyMax(final long[] latencyMax) { + this.mqFaultStrategy.setLatencyMax(latencyMax); + } + + public boolean isSendLatencyFaultEnable() { + return this.mqFaultStrategy.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 new file mode 100644 index 0000000..934a280 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/producer/MQProducerInner.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.client.impl.producer; + +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.remoting.protocol.header.CheckTransactionStateRequestHeader; + +public interface MQProducerInner { + Set getPublishTopicList(); + + boolean isPublishTopicNeedUpdate(final String topic); + + TransactionCheckListener checkListener(); + TransactionListener getCheckListener(); + + void checkTransactionState( + final String addr, + final MessageExt msg, + final CheckTransactionStateRequestHeader checkRequestHeader); + + void updateTopicPublishInfo(final String topic, final TopicPublishInfo info); + + boolean isUnitMode(); +} 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 new file mode 100644 index 0000000..917fe57 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/producer/TopicPublishInfo.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.client.impl.producer; + +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.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 volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); + private TopicRouteData topicRouteData; + + public interface QueueFilter { + boolean filter(MessageQueue mq); + } + + public boolean isOrderTopic() { + return orderTopic; + } + + public void setOrderTopic(boolean orderTopic) { + this.orderTopic = orderTopic; + } + + public boolean ok() { + return null != this.messageQueueList && !this.messageQueueList.isEmpty(); + } + + public List getMessageQueueList() { + return messageQueueList; + } + + public void setMessageQueueList(List messageQueueList) { + this.messageQueueList = messageQueueList; + } + + public ThreadLocalIndex getSendWhichQueue() { + return sendWhichQueue; + } + + public void setSendWhichQueue(ThreadLocalIndex sendWhichQueue) { + this.sendWhichQueue = sendWhichQueue; + } + + public boolean isHaveTopicRouterInfo() { + return haveTopicRouterInfo; + } + + 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 { + for (int i = 0; i < this.messageQueueList.size(); i++) { + MessageQueue mq = selectOneMessageQueue(); + if (!mq.getBrokerName().equals(lastBrokerName)) { + return mq; + } + } + return selectOneMessageQueue(); + } + } + + public MessageQueue selectOneMessageQueue() { + int index = this.sendWhichQueue.incrementAndGet(); + int pos = index % this.messageQueueList.size(); + + return this.messageQueueList.get(pos); + } + + 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)) { + return queueData.getWriteQueueNums(); + } + } + + return -1; + } + + @Override + public String toString() { + return "TopicPublishInfo [orderTopic=" + orderTopic + ", messageQueueList=" + messageQueueList + + ", sendWhichQueue=" + sendWhichQueue + ", haveTopicRouterInfo=" + haveTopicRouterInfo + "]"; + } + + public TopicRouteData getTopicRouteData() { + return topicRouteData; + } + + public void setTopicRouteData(final TopicRouteData topicRouteData) { + this.topicRouteData = topicRouteData; + } +} 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 new file mode 100644 index 0000000..17aaa26 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/latency/LatencyFaultTolerance.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.client.latency; + +public interface LatencyFaultTolerance { + /** + * 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 new file mode 100644 index 0000000..db8bbd6 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/latency/LatencyFaultToleranceImpl.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.client.latency; + +import java.util.Collections; +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; + } + + @Override + 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; + } + } + } + } + + @Override + 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); + } + + @Override + public void shutdown() { + this.scheduledExecutorService.shutdown(); + } + + @Override + 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.updateNotAvailableDuration(notAvailableDuration); + faultItem.setReachable(reachable); + old = this.faultItemTable.putIfAbsent(name, faultItem); + } + + if (null != old) { + old.setCurrentLatency(currentLatency); + old.updateNotAvailableDuration(notAvailableDuration); + old.setReachable(reachable); + } + + if (!reachable) { + log.info(name + " is unreachable, it will not be used until it's reachable"); + } + } + + @Override + public boolean isAvailable(final String name) { + final FaultItem faultItem = this.faultItemTable.get(name); + if (faultItem != null) { + return faultItem.isAvailable(); + } + return true; + } + + @Override + 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); + } + + @Override + public boolean isStartDetectorEnable() { + return startDetectorEnable; + } + + @Override + public void setStartDetectorEnable(boolean startDetectorEnable) { + this.startDetectorEnable = startDetectorEnable; + } + @Override + public String pickOneAtLeast() { + final Enumeration elements = this.faultItemTable.elements(); + List tmpList = new LinkedList(); + while (elements.hasMoreElements()) { + final FaultItem faultItem = elements.nextElement(); + tmpList.add(faultItem); + } + + if (!tmpList.isEmpty()) { + Collections.shuffle(tmpList); + for (FaultItem faultItem : tmpList) { + if (faultItem.reachableFlag) { + return faultItem.name; + } + } + } + + return null; + } + + @Override + public String toString() { + return "LatencyFaultToleranceImpl{" + + "faultItemTable=" + faultItemTable + + ", whichItemWorst=" + whichItemWorst + + '}'; + } + + @Override + public void setDetectTimeout(final int detectTimeout) { + this.detectTimeout = detectTimeout; + } + + @Override + 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()) { + return -1; + } + + if (other.isAvailable()) { + return 1; + } + } + + if (this.currentLatency < other.currentLatency) { + return -1; + } else if (this.currentLatency > other.currentLatency) { + return 1; + } + + if (this.startTimestamp < other.startTimestamp) { + return -1; + } 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; + } + + public boolean isReachable() { + return reachableFlag; + } + + @Override + public int hashCode() { + int result = getName() != null ? getName().hashCode() : 0; + result = 31 * result + (int) (getCurrentLatency() ^ (getCurrentLatency() >>> 32)); + result = 31 * result + (int) (getStartTimestamp() ^ (getStartTimestamp() >>> 32)); + return result; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FaultItem)) { + return false; + } + + final FaultItem faultItem = (FaultItem) o; + + if (getCurrentLatency() != faultItem.getCurrentLatency()) { + return false; + } + 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 + + ", reachableFlag=" + reachableFlag + + '}'; + } + + public String getName() { + return name; + } + + public long getCurrentLatency() { + return currentLatency; + } + + public void setCurrentLatency(final long currentLatency) { + this.currentLatency = currentLatency; + } + + public long getStartTimestamp() { + return startTimestamp; + } + + public void setStartTimestamp(final long startTimestamp) { + this.startTimestamp = startTimestamp; + } + + } +} 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 new file mode 100644 index 0000000..69fb533 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/latency/MQFaultStrategy.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.client.latency; + +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.impl.producer.TopicPublishInfo; +import org.apache.rocketmq.client.impl.producer.TopicPublishInfo.QueueFilter; +import org.apache.rocketmq.common.message.MessageQueue; + +public class MQFaultStrategy { + 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}; + + 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()); + } + + + 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; + } + + public long[] getLatencyMax() { + return latencyMax; + } + + public void setLatencyMax(final long[] latencyMax) { + this.latencyMax = latencyMax; + } + + public boolean isSendLatencyFaultEnable() { + return sendLatencyFaultEnable; + } + + public void setSendLatencyFaultEnable(final boolean sendLatencyFaultEnable) { + this.sendLatencyFaultEnable = sendLatencyFaultEnable; + } + + 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) { + 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(); + } + + MessageQueue mq = tpInfo.selectOneMessageQueue(brokerFilter); + if (mq != null) { + return mq; + } + return tpInfo.selectOneMessageQueue(); + } + + public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation, + final boolean reachable) { + if (this.sendLatencyFaultEnable) { + 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]) { + 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 0000000..1c29ba3 --- /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 0000000..c6ffbad --- /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/lock/ReadWriteCASLock.java b/client/src/main/java/org/apache/rocketmq/client/lock/ReadWriteCASLock.java new file mode 100644 index 0000000..3d15731 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/lock/ReadWriteCASLock.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.client.lock; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +public class ReadWriteCASLock { + //true : can lock ; false : not lock + private final AtomicBoolean writeLock = new AtomicBoolean(true); + + private final AtomicInteger readLock = new AtomicInteger(0); + + public void acquireWriteLock() { + boolean isLock = false; + do { + isLock = writeLock.compareAndSet(true, false); + } while (!isLock); + + do { + isLock = readLock.get() == 0; + } while (!isLock); + } + + public void releaseWriteLock() { + this.writeLock.compareAndSet(false, true); + } + + public void acquireReadLock() { + boolean isLock = false; + do { + isLock = writeLock.get(); + } while (!isLock); + readLock.getAndIncrement(); + } + + public void releaseReadLock() { + this.readLock.getAndDecrement(); + } + + public boolean getWriteLock() { + return this.writeLock.get() && this.readLock.get() == 0; + } + + public boolean getReadLock() { + return this.writeLock.get(); + } + +} 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 new file mode 100644 index 0000000..11edcaa --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/producer/DefaultMQProducer.java @@ -0,0 +1,1491 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.lock.ReadWriteCASLock; +import org.apache.rocketmq.client.trace.hook.DefaultRecallMessageTraceHook; +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.compression.CompressionType; +import org.apache.rocketmq.common.compression.Compressor; +import org.apache.rocketmq.common.compression.CompressorFactory; +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.MessageExt; +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.RPCHook; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.ResponseCode; + +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; + +/** + * 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 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 { + + /** + * 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.SYSTEM_BUSY, + ResponseCode.NO_PERMISSION, + ResponseCode.NO_BUYER_ID, + ResponseCode.NOT_IN_CURRENT_UNIT, + ResponseCode.GO_AWAY + )); + + /** + * 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 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 = TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC; + + /** + * Number of queues to create per default topic. + */ + private volatile int defaultTopicQueueNums = 4; + + /** + * Timeout for sending messages. + */ + private int sendMsgTimeout = 3000; + + /** + * Max timeout for sending messages per request. + */ + private int sendMsgMaxTimeoutPerRequest = -1; + + /** + * Compress message body threshold, namely, message body larger than 4k will be compressed on default. + */ + private int compressMsgBodyOverHowmuch = 1024 * 4; + + /** + * 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.

    + *

    + * This may potentially cause message duplication which is up to application developers to resolve. + */ + private int retryTimesWhenSendAsyncFailed = 2; + + /** + * Indicate whether to retry another broker on sending failure internally. + */ + private boolean retryAnotherBrokerWhenNotStoreOK = false; + + /** + * 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; + + /** + * Maximum hold time of accumulator. + */ + private int batchMaxDelayMs = -1; + + /** + * Maximum accumulation message body size for a single messageAccumulation. + */ + private long batchMaxBytes = -1; + + /** + * Maximum message body size for produceAccumulator. + */ + private long totalBatchMaxBytes = -1; + + private RPCHook rpcHook = null; + + /** + * backPressureForAsyncSendNum is guaranteed to be modified at runtime and no new requests are allowed + */ + private final ReadWriteCASLock backPressureForAsyncSendNumLock = new ReadWriteCASLock(); + + /** + * backPressureForAsyncSendSize is guaranteed to be modified at runtime and no new requests are allowed + */ + private final ReadWriteCASLock backPressureForAsyncSendSizeLock = new ReadWriteCASLock(); + + /** + * Compress level of compress algorithm. + */ + private int compressLevel = Integer.parseInt(System.getProperty(MixAll.MESSAGE_COMPRESS_LEVEL, "5")); + + /** + * Compress type of compress algorithm, default using ZLIB. + */ + private CompressionType compressType = CompressionType.of(System.getProperty(MixAll.MESSAGE_COMPRESS_TYPE, "ZLIB")); + + /** + * Compressor of compress algorithm. + */ + private Compressor compressor = CompressorFactory.getCompressor(compressType); + + /** + * Default constructor. + */ + public DefaultMQProducer() { + 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, (RPCHook) null); + } + + /** + * 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. + */ + public DefaultMQProducer(final String producerGroup, RPCHook rpcHook) { + this(producerGroup, rpcHook, null); + } + + /** + * 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, topics, false, null); + } + + /** + * 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, null, enableMsgTrace, customizedTraceTopic); + } + + /** + * 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 = producerGroup; + this.rpcHook = rpcHook; + this.topics = topics; + this.enableTrace = enableMsgTrace; + this.traceTopic = customizedTraceTopic; + defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook); + } + + /** + * Constructor specifying producer group. + * + * @param namespace Namespace for this MQ Producer instance. + * @param producerGroup Producer group, see the name-sake field. + */ + @Deprecated + public DefaultMQProducer(final String namespace, final String producerGroup) { + this(namespace, producerGroup, null); + } + + /** + * Constructor specifying namespace, producer group and RPC hook. + * + * @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. + */ + @Deprecated + public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook) { + this.namespace = namespace; + this.producerGroup = producerGroup; + this.rpcHook = rpcHook; + defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook); + } + + /** + * 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 + this.enableTrace = enableMsgTrace; + this.traceTopic = customizedTraceTopic; + } + + /** + * 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.

    + * + * @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 (enableTrace) { + try { + AsyncTraceDispatcher dispatcher = new AsyncTraceDispatcher(producerGroup, TraceDispatcher.Type.PRODUCE, getTraceMsgBatchNum(), traceTopic, rpcHook); + dispatcher.setHostProducer(this.defaultMQProducerImpl); + dispatcher.setNamespaceV2(this.namespaceV2); + traceDispatcher = dispatcher; + this.defaultMQProducerImpl.registerSendMessageHook( + new SendMessageTraceHookImpl(traceDispatcher)); + this.defaultMQProducerImpl.registerEndTransactionHook( + new EndTransactionTraceHookImpl(traceDispatcher)); + this.defaultMQProducerImpl.getMqClientFactory().getMQClientAPIImpl().getRemotingClient() + .registerRPCHook(new DefaultRecallMessageTraceHook(traceDispatcher)); + } catch (Throwable e) { + logger.error("system mqtrace hook init failed ,maybe can't send msg trace data"); + } + } + if (null != traceDispatcher) { + if (traceDispatcher instanceof AsyncTraceDispatcher) { + ((AsyncTraceDispatcher) traceDispatcher).getTraceProducer().setUseTLS(isUseTLS()); + } + try { + traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel()); + } catch (MQClientException e) { + logger.warn("trace dispatcher start failed ", e); + } + } + } + + /** + * This method shuts down this producer instance and releases related resources. + */ + @Override + public void shutdown() { + this.defaultMQProducerImpl.shutdown(); + if (this.produceAccumulator != null) { + this.produceAccumulator.shutdown(); + } + if (null != traceDispatcher) { + traceDispatcher.shutdown(); + } + } + + /** + * Fetch message queues of topic topic, to which we may send/publish messages. + * + * @param topic Topic to fetch. + * @return List of message queues readily to send messages to + * @throws MQClientException if there is any client error. + */ + @Override + public List fetchPublishMessageQueues(String topic) throws MQClientException { + 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.

    + * + * 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 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 InterruptedException if the sending thread is interrupted. + */ + @Override + public SendResult send( + Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + 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 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 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.

    + *

    + * 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 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 InterruptedException if the sending thread is interrupted. + */ + @Override + public void send(Message msg, + SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException { + 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 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. + * @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); + } + + /** + * Similar to UDP, this method won't wait for + * 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 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); + } + + /** + * Same to {@link #send(Message)} with target message queue specified in addition. + * + * @param msg Message to send. + * @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 InterruptedException if the sending thread is interrupted. + */ + @Override + public SendResult send(Message msg, MessageQueue mq) + throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + 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 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 InterruptedException if the sending thread is interrupted. + */ + @Override + public SendResult send(Message msg, MessageQueue mq, long timeout) + throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + 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 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 InterruptedException if the sending thread is interrupted. + */ + @Override + public void send(Message msg, MessageQueue mq, SendCallback sendCallback) + throws MQClientException, RemotingException, InterruptedException { + 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 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. + * @throws InterruptedException if the sending thread is interrupted. + */ + @Override + public void send(Message msg, MessageQueue mq, SendCallback sendCallback, long timeout) + throws MQClientException, RemotingException, InterruptedException { + 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. + * @throws InterruptedException if the sending thread is interrupted. + */ + @Override + public void sendOneway(Message msg, + MessageQueue mq) throws MQClientException, RemotingException, InterruptedException { + 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 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. + * @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 InterruptedException if the sending thread is interrupted. + */ + @Override + public SendResult send(Message msg, MessageQueueSelector selector, Object arg) + throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + 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 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. + * @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 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 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 InterruptedException if the sending thread is interrupted. + */ + @Override + public void send(Message msg, MessageQueueSelector selector, Object arg, SendCallback sendCallback) + throws MQClientException, RemotingException, InterruptedException { + 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 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. + * @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 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. + * @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 used to send transactional messages. + * + * @param msg Transactional message to send. + * @param arg Argument used along with local transaction executor. + * @return Transaction result. + * @throws MQClientException + */ + @Override + public TransactionSendResult sendMessageInTransaction(Message msg, + Object arg) throws MQClientException { + throw new RuntimeException("sendMessageInTransaction not implement, please use TransactionMQProducer class"); + } + + /** + * 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 attributes + * @throws MQClientException if there is any client error. + */ + @Deprecated + @Override + 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. 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 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, + 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 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(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(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(queueWithNamespace(mq)); + } + + /** + * 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(queueWithNamespace(mq)); + } + + /** + * 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 maxNum max message number + * @param begin from when + * @param end to when + * @return QueryResult instance contains matched messages. + * @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(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 InterruptedException if the sending thread is interrupted. + */ + @Deprecated + @Override + public MessageExt viewMessage(String topic, + String msgId) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + try { + return this.defaultMQProducerImpl.viewMessage(topic, msgId); + } catch (Exception ignored) { + } + return this.defaultMQProducerImpl.queryMessageByUniqKey(withNamespace(topic), msgId); + } + + @Override + public SendResult send( + Collection msgs) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + return this.defaultMQProducerImpl.send(batch(msgs)); + } + + @Override + public SendResult send(Collection msgs, + long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + return this.defaultMQProducerImpl.send(batch(msgs), timeout); + } + + @Override + public SendResult send(Collection msgs, + MessageQueue messageQueue) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + return this.defaultMQProducerImpl.send(batch(msgs), messageQueue); + } + + @Override + public SendResult send(Collection msgs, MessageQueue messageQueue, + long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + 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); + } + + @Override + public String recallMessage(String topic, String recallHandle) + throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + return this.defaultMQProducerImpl.recallMessage(withNamespace(topic), recallHandle); + } + + /** + * Sets an Executor to be used for executing callback methods. + * + * @param callbackExecutor the instance of Executor + */ + 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 { + msgBatch = MessageBatch.generateFromList(msgs); + 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) { + this.batchMaxDelayMs = holdMs; + if (this.produceAccumulator != null) { + this.produceAccumulator.batchMaxDelayMs(holdMs); + } + } + + public long getBatchMaxBytes() { + if (this.produceAccumulator == null) { + return 0; + } + return produceAccumulator.getBatchMaxBytes(); + } + + public void batchMaxBytes(long holdSize) { + this.batchMaxBytes = holdSize; + if (this.produceAccumulator != null) { + this.produceAccumulator.batchMaxBytes(holdSize); + } + } + + public long getTotalBatchMaxBytes() { + if (this.produceAccumulator == null) { + return 0; + } + return produceAccumulator.getTotalBatchMaxBytes(); + } + + public void totalBatchMaxBytes(long totalHoldSize) { + this.totalBatchMaxBytes = totalHoldSize; + if (this.produceAccumulator != null) { + this.produceAccumulator.totalBatchMaxBytes(totalHoldSize); + } + } + + public boolean getAutoBatch() { + if (this.produceAccumulator == null) { + return false; + } + return this.autoBatch; + } + + public void setAutoBatch(boolean autoBatch) { + this.autoBatch = autoBatch; + } + + public String getProducerGroup() { + return producerGroup; + } + + public void setProducerGroup(String producerGroup) { + this.producerGroup = producerGroup; + } + + public String getCreateTopicKey() { + return createTopicKey; + } + + public void setCreateTopicKey(String createTopicKey) { + this.createTopicKey = createTopicKey; + } + + public int getSendMsgTimeout() { + return sendMsgTimeout; + } + + public void setSendMsgTimeout(int sendMsgTimeout) { + this.sendMsgTimeout = sendMsgTimeout; + } + + public int getSendMsgMaxTimeoutPerRequest() { + return sendMsgMaxTimeoutPerRequest; + } + + public void setSendMsgMaxTimeoutPerRequest(int sendMsgMaxTimeoutPerRequest) { + this.sendMsgMaxTimeoutPerRequest = sendMsgMaxTimeoutPerRequest; + } + + public int getCompressMsgBodyOverHowmuch() { + return compressMsgBodyOverHowmuch; + } + + public void setCompressMsgBodyOverHowmuch(int compressMsgBodyOverHowmuch) { + this.compressMsgBodyOverHowmuch = compressMsgBodyOverHowmuch; + } + + @Deprecated + public DefaultMQProducerImpl getDefaultMQProducerImpl() { + return defaultMQProducerImpl; + } + + public boolean isRetryAnotherBrokerWhenNotStoreOK() { + return retryAnotherBrokerWhenNotStoreOK; + } + + public void setRetryAnotherBrokerWhenNotStoreOK(boolean retryAnotherBrokerWhenNotStoreOK) { + this.retryAnotherBrokerWhenNotStoreOK = retryAnotherBrokerWhenNotStoreOK; + } + + public int getMaxMessageSize() { + return maxMessageSize; + } + + public void setMaxMessageSize(int maxMessageSize) { + this.maxMessageSize = maxMessageSize; + } + + public int getDefaultTopicQueueNums() { + return defaultTopicQueueNums; + } + + public void setDefaultTopicQueueNums(int defaultTopicQueueNums) { + this.defaultTopicQueueNums = defaultTopicQueueNums; + } + + public int getRetryTimesWhenSendFailed() { + return retryTimesWhenSendFailed; + } + + public void setRetryTimesWhenSendFailed(int retryTimesWhenSendFailed) { + this.retryTimesWhenSendFailed = retryTimesWhenSendFailed; + } + + public boolean isSendMessageWithVIPChannel() { + return isVipChannelEnabled(); + } + + public void setSendMessageWithVIPChannel(final boolean sendMessageWithVIPChannel) { + this.setVipChannelEnabled(sendMessageWithVIPChannel); + } + + public long[] getNotAvailableDuration() { + return this.defaultMQProducerImpl.getNotAvailableDuration(); + } + + public void setNotAvailableDuration(final long[] notAvailableDuration) { + this.defaultMQProducerImpl.setNotAvailableDuration(notAvailableDuration); + } + + public long[] getLatencyMax() { + return this.defaultMQProducerImpl.getLatencyMax(); + } + + public void setLatencyMax(final long[] latencyMax) { + this.defaultMQProducerImpl.setLatencyMax(latencyMax); + } + + public boolean isSendLatencyFaultEnable() { + return this.defaultMQProducerImpl.isSendLatencyFaultEnable(); + } + + public void setSendLatencyFaultEnable(final boolean sendLatencyFaultEnable) { + this.defaultMQProducerImpl.setSendLatencyFaultEnable(sendLatencyFaultEnable); + } + + public int getRetryTimesWhenSendAsyncFailed() { + return retryTimesWhenSendAsyncFailed; + } + + 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; + } + + /** + * For user modify backPressureForAsyncSendNum at runtime + */ + public void setBackPressureForAsyncSendNum(int backPressureForAsyncSendNum) { + this.backPressureForAsyncSendNumLock.acquireWriteLock(); + backPressureForAsyncSendNum = Math.max(backPressureForAsyncSendNum, 10); + int acquiredBackPressureForAsyncSendNum = this.backPressureForAsyncSendNum + - defaultMQProducerImpl.getSemaphoreAsyncSendNumAvailablePermits(); + this.backPressureForAsyncSendNum = backPressureForAsyncSendNum; + defaultMQProducerImpl.setSemaphoreAsyncSendNum(backPressureForAsyncSendNum - acquiredBackPressureForAsyncSendNum); + this.backPressureForAsyncSendNumLock.releaseWriteLock(); + } + + public int getBackPressureForAsyncSendSize() { + return backPressureForAsyncSendSize; + } + + /** + * For user modify backPressureForAsyncSendSize at runtime + */ + public void setBackPressureForAsyncSendSize(int backPressureForAsyncSendSize) { + this.backPressureForAsyncSendSizeLock.acquireWriteLock(); + backPressureForAsyncSendSize = Math.max(backPressureForAsyncSendSize, 1024 * 1024); + int acquiredBackPressureForAsyncSendSize = this.backPressureForAsyncSendSize + - defaultMQProducerImpl.getSemaphoreAsyncSendSizeAvailablePermits(); + this.backPressureForAsyncSendSize = backPressureForAsyncSendSize; + defaultMQProducerImpl.setSemaphoreAsyncSendSize(backPressureForAsyncSendSize - acquiredBackPressureForAsyncSendSize); + this.backPressureForAsyncSendSizeLock.releaseWriteLock(); + } + + /** + * Used for system internal adjust backPressureForAsyncSendSize + */ + public void setBackPressureForAsyncSendSizeInsideAdjust(int backPressureForAsyncSendSize) { + this.backPressureForAsyncSendSize = backPressureForAsyncSendSize; + } + + /** + * Used for system internal adjust backPressureForAsyncSendNum + */ + public void setBackPressureForAsyncSendNumInsideAdjust(int backPressureForAsyncSendNum) { + this.backPressureForAsyncSendNum = backPressureForAsyncSendNum; + } + + public void acquireBackPressureForAsyncSendSizeLock() { + this.backPressureForAsyncSendSizeLock.acquireReadLock(); + } + + public void releaseBackPressureForAsyncSendSizeLock() { + this.backPressureForAsyncSendSizeLock.releaseReadLock(); + } + + public void acquireBackPressureForAsyncSendNumLock() { + this.backPressureForAsyncSendNumLock.acquireReadLock(); + } + + public void releaseBackPressureForAsyncSendNumLock() { + this.backPressureForAsyncSendNumLock.releaseReadLock(); + } + + 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); + } + + public int getCompressLevel() { + return compressLevel; + } + + public void setCompressLevel(int compressLevel) { + this.compressLevel = compressLevel; + } + + public CompressionType getCompressType() { + return compressType; + } + + public void setCompressType(CompressionType compressType) { + this.compressType = compressType; + this.compressor = CompressorFactory.getCompressor(compressType); + } + + public Compressor getCompressor() { + return compressor; + } + + public void initProduceAccumulator() { + this.produceAccumulator = MQClientManager.getInstance().getOrCreateProduceAccumulator(this); + + if (this.batchMaxDelayMs > -1) { + this.produceAccumulator.batchMaxDelayMs(this.batchMaxDelayMs); + } + + if (this.batchMaxBytes > -1) { + this.produceAccumulator.batchMaxBytes(this.batchMaxBytes); + } + + if (this.totalBatchMaxBytes > -1) { + this.produceAccumulator.totalBatchMaxBytes(this.totalBatchMaxBytes); + } + + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/producer/LocalTransactionState.java b/client/src/main/java/org/apache/rocketmq/client/producer/LocalTransactionState.java new file mode 100644 index 0000000..9656fb2 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/producer/LocalTransactionState.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.producer; + +public enum LocalTransactionState { + COMMIT_MESSAGE, + ROLLBACK_MESSAGE, + UNKNOW, +} 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 new file mode 100644 index 0000000..4286fdd --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/producer/MQProducer.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.client.producer; + +import java.util.Collection; +import java.util.List; +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; + +public interface MQProducer extends MQAdmin { + void start() throws MQClientException; + + void shutdown(); + + List fetchPublishMessageQueues(final String topic) throws MQClientException; + + SendResult send(final Message msg) throws MQClientException, RemotingException, MQBrokerException, + InterruptedException; + + 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, MQBrokerException; + + void send(final Message msg, final SendCallback sendCallback, final long timeout) + throws MQClientException, RemotingException, InterruptedException; + + void sendOneway(final Message msg) throws MQClientException, RemotingException, + InterruptedException; + + SendResult send(final Message msg, final MessageQueue mq) throws MQClientException, + RemotingException, MQBrokerException, InterruptedException; + + SendResult send(final Message msg, final MessageQueue mq, final long timeout) + throws MQClientException, RemotingException, MQBrokerException, InterruptedException; + + void send(final Message msg, final MessageQueue mq, final SendCallback sendCallback) + throws MQClientException, RemotingException, InterruptedException; + + void send(final Message msg, final MessageQueue mq, final SendCallback sendCallback, long timeout) + throws MQClientException, RemotingException, InterruptedException; + + void sendOneway(final Message msg, final MessageQueue mq) throws MQClientException, + RemotingException, InterruptedException; + + SendResult send(final Message msg, final MessageQueueSelector selector, final Object arg) + throws MQClientException, RemotingException, MQBrokerException, InterruptedException; + + SendResult send(final Message msg, final MessageQueueSelector selector, final Object arg, + final long timeout) throws MQClientException, RemotingException, MQBrokerException, + InterruptedException; + + void send(final Message msg, final MessageQueueSelector selector, final Object arg, + final SendCallback sendCallback) throws MQClientException, RemotingException, + InterruptedException; + + void send(final Message msg, final MessageQueueSelector selector, final Object arg, + final SendCallback sendCallback, final long timeout) throws MQClientException, RemotingException, + InterruptedException; + + void sendOneway(final Message msg, final MessageQueueSelector selector, final Object arg) + throws MQClientException, RemotingException, InterruptedException; + + TransactionSendResult sendMessageInTransaction(final Message msg, + final Object arg) throws MQClientException; + + //for batch + SendResult send(final Collection msgs) throws MQClientException, RemotingException, MQBrokerException, + InterruptedException; + + SendResult send(final Collection msgs, final long timeout) throws MQClientException, + RemotingException, MQBrokerException, InterruptedException; + + SendResult send(final Collection msgs, final MessageQueue mq) throws MQClientException, + RemotingException, MQBrokerException, InterruptedException; + + 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; + + String recallMessage(String topic, String recallHandle) + 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/MessageQueueSelector.java b/client/src/main/java/org/apache/rocketmq/client/producer/MessageQueueSelector.java new file mode 100644 index 0000000..f88dda9 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/producer/MessageQueueSelector.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.client.producer; + +import java.util.List; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageQueue; + +public interface MessageQueueSelector { + MessageQueue select(final List mqs, final Message msg, final Object arg); +} 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 0000000..809830e --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/producer/ProduceAccumulator.java @@ -0,0 +1,526 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 final Map syncSendBatchs = new ConcurrentHashMap(); + private final Map asyncSendBatchs = new ConcurrentHashMap(); + private final 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) { + int bodySize = null == message.getBody() ? 0 : message.getBody().length; + if (bodySize > 0) { + currentlyHoldSize.addAndGet(bodySize); + } + 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 final 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); + int bodySize = null == msg.getBody() ? 0 : msg.getBody().length; + if (bodySize > 0) { + messagesSize.addAndGet(bodySize); + } + 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); + int bodySize = null == msg.getBody() ? 0 : msg.getBody().length; + if (bodySize > 0) { + messagesSize.addAndGet(bodySize); + } + } + 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 0000000..3107ba5 --- /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 0000000..00f5bb6 --- /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 0000000..203f929 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/producer/RequestResponseFuture.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.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 void acquireCountDownLatch() { + this.countDownLatch.countDown(); + } + + 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/SendCallback.java b/client/src/main/java/org/apache/rocketmq/client/producer/SendCallback.java new file mode 100644 index 0000000..a85fa69 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/producer/SendCallback.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.producer; + +public interface SendCallback { + void onSuccess(final SendResult sendResult); + + void onException(final Throwable e); +} 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 new file mode 100644 index 0000000..d160eb4 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/producer/SendResult.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 com.alibaba.fastjson.JSON; +import org.apache.rocketmq.common.message.MessageQueue; + +public class SendResult { + private SendStatus sendStatus; + private String msgId; + private MessageQueue messageQueue; + private long queueOffset; + private String transactionId; + private String offsetMsgId; + private String regionId; + private boolean traceOn = true; + private byte[] rawRespBody; + private String recallHandle; + + public SendResult() { + } + + public SendResult(SendStatus sendStatus, String msgId, String offsetMsgId, MessageQueue messageQueue, + long queueOffset) { + this.sendStatus = sendStatus; + this.msgId = msgId; + this.offsetMsgId = offsetMsgId; + this.messageQueue = messageQueue; + this.queueOffset = queueOffset; + } + + public SendResult(final SendStatus sendStatus, final String msgId, final MessageQueue messageQueue, + final long queueOffset, final String transactionId, + final String offsetMsgId, final String regionId) { + this.sendStatus = sendStatus; + this.msgId = msgId; + this.messageQueue = messageQueue; + this.queueOffset = queueOffset; + this.transactionId = transactionId; + this.offsetMsgId = offsetMsgId; + this.regionId = regionId; + } + + public static String encoderSendResultToJson(final Object obj) { + return JSON.toJSONString(obj); + } + + public static SendResult decoderSendResultFromJson(String json) { + return JSON.parseObject(json, SendResult.class); + } + + public boolean isTraceOn() { + return traceOn; + } + + public void setTraceOn(final boolean traceOn) { + this.traceOn = traceOn; + } + + public String getRegionId() { + return regionId; + } + + public void setRegionId(final String regionId) { + this.regionId = regionId; + } + + public String getMsgId() { + return msgId; + } + + public void setMsgId(String msgId) { + this.msgId = msgId; + } + + public SendStatus getSendStatus() { + return sendStatus; + } + + public void setSendStatus(SendStatus sendStatus) { + this.sendStatus = sendStatus; + } + + public MessageQueue getMessageQueue() { + return messageQueue; + } + + public void setMessageQueue(MessageQueue messageQueue) { + this.messageQueue = messageQueue; + } + + 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 getOffsetMsgId() { + return offsetMsgId; + } + + public void setOffsetMsgId(String offsetMsgId) { + this.offsetMsgId = offsetMsgId; + } + + public String getRecallHandle() { + return recallHandle; + } + + public void setRecallHandle(String recallHandle) { + this.recallHandle = recallHandle; + } + + @Override + public String toString() { + return "SendResult [sendStatus=" + sendStatus + ", msgId=" + msgId + ", offsetMsgId=" + offsetMsgId + ", messageQueue=" + messageQueue + + ", queueOffset=" + queueOffset + ", recallHandle=" + recallHandle + "]"; + } + + 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/SendStatus.java b/client/src/main/java/org/apache/rocketmq/client/producer/SendStatus.java new file mode 100644 index 0000000..ee5ecfe --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/producer/SendStatus.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.producer; + +public enum SendStatus { + SEND_OK, + FLUSH_DISK_TIMEOUT, + FLUSH_SLAVE_TIMEOUT, + SLAVE_NOT_AVAILABLE, +} 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 new file mode 100644 index 0000000..5f59506 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/producer/TransactionCheckListener.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.client.producer; + +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 0000000..233af69 --- /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 new file mode 100644 index 0000000..5c7b437 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/producer/TransactionMQProducer.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.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; + private int checkThreadPoolMinSize = 1; + private int checkThreadPoolMaxSize = 1; + private int checkRequestHoldMax = 2000; + + private ExecutorService executorService; + + private TransactionListener transactionListener; + + public TransactionMQProducer() { + } + + 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, 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 + public void start() throws MQClientException { + this.defaultMQProducerImpl.initTransactionEnv(); + super.start(); + } + + @Override + public void shutdown() { + super.shutdown(); + this.defaultMQProducerImpl.destroyTransactionEnv(); + } + + @Override + public TransactionSendResult sendMessageInTransaction(final Message msg, + final Object arg) throws MQClientException { + if (null == this.transactionListener) { + throw new MQClientException("TransactionListener is null", null); + } + + 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; + } + + 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; + } + + 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; + } + + 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/TransactionSendResult.java b/client/src/main/java/org/apache/rocketmq/client/producer/TransactionSendResult.java new file mode 100644 index 0000000..1037337 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/producer/TransactionSendResult.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.client.producer; + +public class TransactionSendResult extends SendResult { + private LocalTransactionState localTransactionState; + + public TransactionSendResult() { + } + + public LocalTransactionState getLocalTransactionState() { + return localTransactionState; + } + + public void setLocalTransactionState(LocalTransactionState localTransactionState) { + this.localTransactionState = localTransactionState; + } +} 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 new file mode 100644 index 0000000..ba8ea8b --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/producer/selector/SelectMessageQueueByHash.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.producer.selector; + +import java.util.List; +import org.apache.rocketmq.client.producer.MessageQueueSelector; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageQueue; + +public class SelectMessageQueueByHash implements MessageQueueSelector { + + @Override + public MessageQueue select(List mqs, Message msg, Object arg) { + int value = arg.hashCode() % mqs.size(); + if (value < 0) { + value = Math.abs(value); + } + return mqs.get(value); + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/producer/selector/SelectMessageQueueByMachineRoom.java b/client/src/main/java/org/apache/rocketmq/client/producer/selector/SelectMessageQueueByMachineRoom.java new file mode 100644 index 0000000..fb262ea --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/producer/selector/SelectMessageQueueByMachineRoom.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.selector; + +import java.util.List; +import java.util.Set; +import org.apache.rocketmq.client.producer.MessageQueueSelector; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageQueue; + +public class SelectMessageQueueByMachineRoom implements MessageQueueSelector { + private Set consumeridcs; + + @Override + public MessageQueue select(List mqs, Message msg, Object arg) { + return null; + } + + public Set getConsumeridcs() { + return consumeridcs; + } + + public void setConsumeridcs(Set consumeridcs) { + this.consumeridcs = consumeridcs; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/producer/selector/SelectMessageQueueByRandom.java b/client/src/main/java/org/apache/rocketmq/client/producer/selector/SelectMessageQueueByRandom.java new file mode 100644 index 0000000..070fcc3 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/producer/selector/SelectMessageQueueByRandom.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.producer.selector; + +import java.util.List; +import java.util.Random; +import org.apache.rocketmq.client.producer.MessageQueueSelector; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageQueue; + +public class SelectMessageQueueByRandom implements MessageQueueSelector { + private Random random = new Random(System.currentTimeMillis()); + + @Override + public MessageQueue select(List mqs, Message msg, Object arg) { + int value = random.nextInt(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 0000000..0178b2c --- /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 new file mode 100644 index 0000000..a9f506e --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/stat/ConsumerStatsManager.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.client.stat; + +import java.util.concurrent.ScheduledExecutorService; +import org.apache.rocketmq.common.stats.StatsItemSet; +import org.apache.rocketmq.common.stats.StatsSnapshot; +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(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"; + private static final String TOPIC_AND_GROUP_CONSUME_RT = "CONSUME_RT"; + private static final String TOPIC_AND_GROUP_PULL_TPS = "PULL_TPS"; + private static final String TOPIC_AND_GROUP_PULL_RT = "PULL_RT"; + + private final StatsItemSet topicAndGroupConsumeOKTPS; + private final StatsItemSet topicAndGroupConsumeRT; + private final StatsItemSet topicAndGroupConsumeFailedTPS; + private final StatsItemSet topicAndGroupPullTPS; + private final StatsItemSet topicAndGroupPullRT; + + public ConsumerStatsManager(final ScheduledExecutorService scheduledExecutorService) { + this.topicAndGroupConsumeOKTPS = + new StatsItemSet(TOPIC_AND_GROUP_CONSUME_OK_TPS, scheduledExecutorService, log); + + this.topicAndGroupConsumeRT = + new StatsItemSet(TOPIC_AND_GROUP_CONSUME_RT, scheduledExecutorService, log); + + this.topicAndGroupConsumeFailedTPS = + new StatsItemSet(TOPIC_AND_GROUP_CONSUME_FAILED_TPS, scheduledExecutorService, log); + + this.topicAndGroupPullTPS = new StatsItemSet(TOPIC_AND_GROUP_PULL_TPS, scheduledExecutorService, log); + + this.topicAndGroupPullRT = new StatsItemSet(TOPIC_AND_GROUP_PULL_RT, scheduledExecutorService, log); + } + + public void start() { + } + + public void shutdown() { + } + + public void incPullRT(final String group, final String topic, final long rt) { + this.topicAndGroupPullRT.addRTValue(topic + "@" + group, (int) rt, 1); + } + + public void incPullTPS(final String group, final String topic, final long msgs) { + this.topicAndGroupPullTPS.addValue(topic + "@" + group, (int) msgs, 1); + } + + public void incConsumeRT(final String group, final String topic, final long rt) { + this.topicAndGroupConsumeRT.addRTValue(topic + "@" + group, (int) rt, 1); + } + + public void incConsumeOKTPS(final String group, final String topic, final long msgs) { + this.topicAndGroupConsumeOKTPS.addValue(topic + "@" + group, (int) msgs, 1); + } + + public void incConsumeFailedTPS(final String group, final String topic, final long msgs) { + this.topicAndGroupConsumeFailedTPS.addValue(topic + "@" + group, (int) msgs, 1); + } + + public ConsumeStatus consumeStatus(final String group, final String topic) { + ConsumeStatus cs = new ConsumeStatus(); + { + StatsSnapshot ss = this.getPullRT(group, topic); + if (ss != null) { + cs.setPullRT(ss.getAvgpt()); + } + } + + { + StatsSnapshot ss = this.getPullTPS(group, topic); + if (ss != null) { + cs.setPullTPS(ss.getTps()); + } + } + + { + StatsSnapshot ss = this.getConsumeRT(group, topic); + if (ss != null) { + cs.setConsumeRT(ss.getAvgpt()); + } + } + + { + StatsSnapshot ss = this.getConsumeOKTPS(group, topic); + if (ss != null) { + cs.setConsumeOKTPS(ss.getTps()); + } + } + + { + StatsSnapshot ss = this.getConsumeFailedTPS(group, topic); + if (ss != null) { + cs.setConsumeFailedTPS(ss.getTps()); + } + } + + { + StatsSnapshot ss = this.topicAndGroupConsumeFailedTPS.getStatsDataInHour(topic + "@" + group); + if (ss != null) { + cs.setConsumeFailedMsgs(ss.getSum()); + } + } + + return cs; + } + + private StatsSnapshot getPullRT(final String group, final String topic) { + return this.topicAndGroupPullRT.getStatsDataInMinute(topic + "@" + group); + } + + private StatsSnapshot getPullTPS(final String group, final String topic) { + return this.topicAndGroupPullTPS.getStatsDataInMinute(topic + "@" + group); + } + + private StatsSnapshot getConsumeRT(final String group, final String topic) { + StatsSnapshot statsData = this.topicAndGroupConsumeRT.getStatsDataInMinute(topic + "@" + group); + if (0 == statsData.getSum()) { + statsData = this.topicAndGroupConsumeRT.getStatsDataInHour(topic + "@" + group); + } + + return statsData; + } + + private StatsSnapshot getConsumeOKTPS(final String group, final String topic) { + return this.topicAndGroupConsumeOKTPS.getStatsDataInMinute(topic + "@" + group); + } + + private StatsSnapshot getConsumeFailedTPS(final String group, final String topic) { + return this.topicAndGroupConsumeFailedTPS.getStatsDataInMinute(topic + "@" + group); + } +} 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 0000000..c76fea7 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/trace/AsyncTraceDispatcher.java @@ -0,0 +1,422 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Map; +import java.util.Set; +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.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.RPCHook; + +import static org.apache.rocketmq.client.trace.TraceConstants.TRACE_INSTANCE_NAME; + +public class AsyncTraceDispatcher implements TraceDispatcher { + private static final Logger log = LoggerFactory.getLogger(AsyncTraceDispatcher.class); + private static final AtomicInteger COUNTER = new AtomicInteger(); + private static final AtomicInteger INSTANCE_NUM = new AtomicInteger(0); + private static final long WAIT_FOR_SHUTDOWN = 5000L; + private volatile boolean stopped = false; + private final int traceInstanceId = INSTANCE_NUM.getAndIncrement(); + private final int batchNum; + private final int maxMsgSize; + private final DefaultMQProducer traceProducer; + private AtomicLong discardCount; + private Thread worker; + private final ThreadPoolExecutor traceExecutor; + private final ArrayBlockingQueue traceContextQueue; + private final ArrayBlockingQueue appenderQueue; + private volatile Thread shutDownHook; + + private DefaultMQProducerImpl hostProducer; + private DefaultMQPushConsumerImpl hostConsumer; + private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); + private volatile String traceTopicName; + private AtomicBoolean isStarted = new AtomicBoolean(false); + private volatile AccessChannel accessChannel = AccessChannel.LOCAL; + private String group; + private Type type; + private String namespaceV2; + private final int flushTraceInterval = 5000; + + private long lastFlushTime = System.currentTimeMillis(); + + public AsyncTraceDispatcher(String group, Type type, int batchNum, String traceTopicName, RPCHook rpcHook) { + this.batchNum = Math.min(batchNum, 20);/* max value 20*/ + this.maxMsgSize = 128000; + this.discardCount = new AtomicLong(0L); + this.traceContextQueue = new ArrayBlockingQueue<>(2048); + this.group = group; + this.type = type; + this.appenderQueue = new ArrayBlockingQueue<>(2048); + if (!UtilAll.isBlank(traceTopicName)) { + this.traceTopicName = traceTopicName; + } else { + this.traceTopicName = TopicValidator.RMQ_SYS_TRACE_TOPIC; + } + this.traceExecutor = new ThreadPoolExecutor(// + 2, // + 4, // + 1000 * 60, // + TimeUnit.MILLISECONDS, // + this.appenderQueue, // + new ThreadFactoryImpl("MQTraceSendThread_" + traceInstanceId + "_")); + 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 String getNamespaceV2() { + return namespaceV2; + } + + public void setNamespaceV2(String namespaceV2) { + this.namespaceV2 = namespaceV2; + } + + public void start(String nameSrvAddr, AccessChannel accessChannel) throws MQClientException { + if (isStarted.compareAndSet(false, true)) { + traceProducer.setNamesrvAddr(nameSrvAddr); + traceProducer.setInstanceName(TRACE_INSTANCE_NAME + "_" + nameSrvAddr); + traceProducer.setNamespaceV2(namespaceV2); + traceProducer.setEnableTrace(false); + traceProducer.start(); + } + this.accessChannel = accessChannel; + this.worker = new ThreadFactoryImpl("MQ-AsyncArrayDispatcher-Thread" + traceInstanceId, true) + .newThread(new AsyncRunnable()); + 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() { + while (traceContextQueue.size() > 0) { + try { + flushTraceContext(true); + } catch (Throwable throwable) { + log.error("flushTraceContext error", throwable); + } + } + } + + @Override + public void shutdown() { + flush(); + ThreadUtils.shutdownGracefully(this.traceExecutor, WAIT_FOR_SHUTDOWN, TimeUnit.MILLISECONDS); + if (isStarted.get()) { + traceProducer.shutdown(); + } + this.removeShutdownHook(); + stopped = true; + } + + 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 volatile boolean stopped = false; + + @Override + public void run() { + while (!stopped) { + try { + flushTraceContext(false); + } catch (Throwable e) { + log.error("flushTraceContext error", e); + } + + if (AsyncTraceDispatcher.this.stopped) { + this.stopped = true; + } + } + } + } + + private void flushTraceContext(boolean forceFlush) throws InterruptedException { + List contextList = new ArrayList<>(batchNum); + int size = traceContextQueue.size(); + if (size != 0) { + if (forceFlush || size >= batchNum || System.currentTimeMillis() - lastFlushTime > flushTraceInterval) { + for (int i = 0; i < batchNum; i++) { + TraceContext context = traceContextQueue.poll(); + if (context != null) { + contextList.add(context); + } else { + break; + } + } + asyncSendTraceMessage(contextList); + return; + } + } + // To prevent an infinite loop, add a wait time between each two task executions + Thread.sleep(5); + } + + private void asyncSendTraceMessage(List contextList) { + AsyncDataSendTask request = new AsyncDataSendTask(contextList); + traceExecutor.submit(request); + lastFlushTime = System.currentTimeMillis(); + } + + class AsyncDataSendTask implements Runnable { + private final List contextList; + + public AsyncDataSendTask(List contextList) { + this.contextList = contextList; + } + + @Override + public void run() { + sendTraceData(contextList); + } + + public void sendTraceData(List contextList) { + Map> transBeanMap = new HashMap<>(16); + String traceTopic; + for (TraceContext context : contextList) { + AccessChannel accessChannel = context.getAccessChannel(); + if (accessChannel == null) { + accessChannel = AsyncTraceDispatcher.this.accessChannel; + } + String currentRegionId = context.getRegionId(); + if (currentRegionId == null || context.getTraceBeans().isEmpty()) { + continue; + } + if (AccessChannel.CLOUD == accessChannel) { + traceTopic = TraceConstants.TRACE_TOPIC_PREFIX + currentRegionId; + } else { + traceTopic = traceTopicName; + } + + String topic = context.getTraceBeans().get(0).getTopic(); + String key = topic + TraceConstants.CONTENT_SPLITOR + traceTopic; + List transBeanList = transBeanMap.computeIfAbsent(key, k -> new ArrayList<>()); + TraceTransferBean traceData = TraceDataEncoder.encoderFromContextBean(context); + transBeanList.add(traceData); + } + for (Map.Entry> entry : transBeanMap.entrySet()) { + String[] key = entry.getKey().split(String.valueOf(TraceConstants.CONTENT_SPLITOR)); + flushData(entry.getValue(), key[0], key[1]); + } + } + + private void flushData(List transBeanList, String topic, String traceTopic) { + if (transBeanList.size() == 0) { + return; + } + StringBuilder buffer = new StringBuilder(1024); + int count = 0; + Set keySet = new HashSet(); + for (TraceTransferBean bean : transBeanList) { + keySet.addAll(bean.getTransKey()); + buffer.append(bean.getTransData()); + count++; + if (buffer.length() >= traceProducer.getMaxMessageSize()) { + sendTraceDataByMQ(keySet, buffer.toString(), traceTopic); + buffer.delete(0, buffer.length()); + keySet.clear(); + count = 0; + } + } + if (count > 0) { + sendTraceDataByMQ(keySet, buffer.toString(), traceTopic); + } + transBeanList.clear(); + } + + /** + * 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; + } + } + +} \ No newline at end of file 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 0000000..17db1fb --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/trace/TraceBean.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 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; + 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; + + static { + byte[] ip = UtilAll.getIP(); + if (ip.length == 4) { + LOCAL_ADDRESS = UtilAll.ipToIPv4Str(ip); + } else { + LOCAL_ADDRESS = UtilAll.ipToIPv6Str(ip); + } + } + + 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 0000000..67f7ab3 --- /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_STORE_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 0000000..a1f632e --- /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 0000000..1e66aa0 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/trace/TraceDataEncoder.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.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); + } else if (line[0].equals(TraceType.Recall.name())) { + TraceContext recallContext = new TraceContext(); + recallContext.setTraceType(TraceType.Recall); + recallContext.setTimeStamp(Long.parseLong(line[1])); + recallContext.setRegionId(line[2]); + recallContext.setGroupName(line[3]); + TraceBean bean = new TraceBean(); + bean.setTopic(line[4]); + bean.setMsgId(line[5]); + recallContext.setSuccess(Boolean.parseBoolean(line[6])); + recallContext.setTraceBeans(new ArrayList<>(1)); + recallContext.getTraceBeans().add(bean); + resList.add(recallContext); + } + } + 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); + sb.append(ctx.getGroupName()); + } + sb.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; + case Recall: { + 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(ctx.isSuccess()).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 0000000..ac4ef3b --- /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 0000000..f09c9b8 --- /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 0000000..482a782 --- /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 0000000..4c0e7d8 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/trace/TraceType.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.client.trace; + +public enum TraceType { + Pub, + Recall, + 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 0000000..01ce566 --- /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 0000000..b983df3 --- /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 0000000..f23a4ff --- /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/DefaultRecallMessageTraceHook.java b/client/src/main/java/org/apache/rocketmq/client/trace/hook/DefaultRecallMessageTraceHook.java new file mode 100644 index 0000000..c490a7b --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/trace/hook/DefaultRecallMessageTraceHook.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.client.trace.hook; + +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.message.MessageConst; +import org.apache.rocketmq.common.producer.RecallMessageHandle; +import org.apache.rocketmq.remoting.RPCHook; +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.RecallMessageRequestHeader; + +import java.util.ArrayList; + +public class DefaultRecallMessageTraceHook implements RPCHook { + + private static final String RECALL_TRACE_ENABLE_KEY = "com.rocketmq.recall.default.trace.enable"; + private boolean enableDefaultTrace = Boolean.parseBoolean(System.getProperty(RECALL_TRACE_ENABLE_KEY, "false")); + private TraceDispatcher traceDispatcher; + + public DefaultRecallMessageTraceHook(TraceDispatcher traceDispatcher) { + this.traceDispatcher = traceDispatcher; + } + + @Override + public void doBeforeRequest(String remoteAddr, RemotingCommand request) { + } + + @Override + public void doAfterResponse(String remoteAddr, RemotingCommand request, RemotingCommand response) { + if (request.getCode() != RequestCode.RECALL_MESSAGE + || !enableDefaultTrace + || null == response.getExtFields() + || null == response.getExtFields().get(MessageConst.PROPERTY_MSG_REGION) + || null == traceDispatcher) { + return; + } + + try { + String regionId = response.getExtFields().get(MessageConst.PROPERTY_MSG_REGION); + RecallMessageRequestHeader requestHeader = + request.decodeCommandCustomHeader(RecallMessageRequestHeader.class); + String topic = NamespaceUtil.withoutNamespace(requestHeader.getTopic()); + String group = NamespaceUtil.withoutNamespace(requestHeader.getProducerGroup()); + String recallHandle = requestHeader.getRecallHandle(); + RecallMessageHandle.HandleV1 handleV1 = + (RecallMessageHandle.HandleV1) RecallMessageHandle.decodeHandle(recallHandle); + + TraceBean traceBean = new TraceBean(); + traceBean.setTopic(topic); + traceBean.setMsgId(handleV1.getMessageId()); + + TraceContext traceContext = new TraceContext(); + traceContext.setRegionId(regionId); + traceContext.setTraceBeans(new ArrayList<>(1)); + traceContext.setTraceType(TraceType.Recall); + traceContext.setGroupName(group); + traceContext.getTraceBeans().add(traceBean); + traceContext.setSuccess(ResponseCode.SUCCESS == response.getCode()); + + traceDispatcher.append(traceContext); + } catch (Exception e) { + } + } +} 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 0000000..44e4b69 --- /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_STORE_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 0000000..d69388e --- /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 0000000..0f828f2 --- /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_STORE_HOST, context.getBrokerAddr()); + span.setTag(TraceConstants.ROCKETMQ_MSG_TYPE, context.getMsgType().name()); + span.setTag(TraceConstants.ROCKETMQ_BODY_LENGTH, null == msg.getBody() ? 0 : 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 0000000..6173892 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/trace/hook/SendMessageTraceHookImpl.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.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()); + int bodyLength = null == context.getMessage().getBody() ? 0 : context.getMessage().getBody().length; + traceBean.setBodyLength(bodyLength); + 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 0000000..416ba44 --- /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/rmq.client.logback.xml b/client/src/main/resources/rmq.client.logback.xml new file mode 100644 index 0000000..8e57d8d --- /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/acl/common/AclClientRPCHookTest.java b/client/src/test/java/org/apache/rocketmq/acl/common/AclClientRPCHookTest.java new file mode 100644 index 0000000..8d99407 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/acl/common/AclClientRPCHookTest.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.acl.common; + +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 java.lang.reflect.Field; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; + +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/client/src/test/java/org/apache/rocketmq/acl/common/AclSignerTest.java b/client/src/test/java/org/apache/rocketmq/acl/common/AclSignerTest.java new file mode 100644 index 0000000..2680d6b --- /dev/null +++ b/client/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/client/src/test/java/org/apache/rocketmq/acl/common/AclUtilsTest.java b/client/src/test/java/org/apache/rocketmq/acl/common/AclUtilsTest.java new file mode 100644 index 0000000..2169144 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/acl/common/AclUtilsTest.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.acl.common; + +import com.alibaba.fastjson2.JSONObject; +import org.apache.commons.lang3.StringUtils; +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.Objects; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +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"); + } + + 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 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); + } + } + + @Test + public void testGetAclRPCHookByFileName() { + RPCHook actual = AclUtils.getAclRPCHook(Objects.requireNonNull(AclUtilsTest.class.getResource("/acl_hook/plain_acl.yml")).getPath()); + assertNotNull(actual); + assertTrue(actual instanceof AclClientRPCHook); + assertAclClientRPCHook((AclClientRPCHook) actual); + } + + @Test + public void testGetAclRPCHookByInputStream() { + RPCHook actual = AclUtils.getAclRPCHook(Objects.requireNonNull(AclUtilsTest.class.getResourceAsStream("/acl_hook/plain_acl.yml"))); + assertNotNull(actual); + assertTrue(actual instanceof AclClientRPCHook); + assertAclClientRPCHook((AclClientRPCHook) actual); + } + + private void assertAclClientRPCHook(final AclClientRPCHook actual) { + assertEquals("rocketmq2", actual.getSessionCredentials().getAccessKey()); + assertEquals("12345678", actual.getSessionCredentials().getSecretKey()); + } +} diff --git a/client/src/test/java/org/apache/rocketmq/acl/common/PermissionTest.java b/client/src/test/java/org/apache/rocketmq/acl/common/PermissionTest.java new file mode 100644 index 0000000..d23f11b --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/acl/common/PermissionTest.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; + +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 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"); + } +} diff --git a/client/src/test/java/org/apache/rocketmq/acl/common/SessionCredentialsTest.java b/client/src/test/java/org/apache/rocketmq/acl/common/SessionCredentialsTest.java new file mode 100644 index 0000000..79512f1 --- /dev/null +++ b/client/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/client/src/test/java/org/apache/rocketmq/client/ClientConfigTest.java b/client/src/test/java/org/apache/rocketmq/client/ClientConfigTest.java new file mode 100644 index 0000000..5afe9cc --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/ClientConfigTest.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; + +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(MockitoJUnitRunner.class) +public class ClientConfigTest { + + private ClientConfig clientConfig; + + private final String resource = "resource"; + + @Before + public void init() { + clientConfig = createClientConfig(); + } + + @Test + public void testWithNamespace() { + Set resources = clientConfig.withNamespace(Collections.singleton(resource)); + assertTrue(resources.contains("lmq%resource")); + } + + @Test + public void testWithoutNamespace() { + String actual = clientConfig.withoutNamespace(resource); + assertEquals(resource, actual); + Set resources = clientConfig.withoutNamespace(Collections.singleton(resource)); + assertTrue(resources.contains(resource)); + } + + @Test + public void testQueuesWithNamespace() { + MessageQueue messageQueue = new MessageQueue(); + messageQueue.setTopic("defaultTopic"); + Collection messageQueues = clientConfig.queuesWithNamespace(Collections.singleton(messageQueue)); + assertTrue(messageQueues.contains(messageQueue)); + assertEquals("lmq%defaultTopic", messageQueues.iterator().next().getTopic()); + } + + private ClientConfig createClientConfig() { + ClientConfig result = new ClientConfig(); + result.setUnitName("unitName"); + result.setClientIP("127.0.0.1"); + result.setClientCallbackExecutorThreads(1); + result.setPollNameServerInterval(1000 * 30); + result.setHeartbeatBrokerInterval(1000 * 30); + result.setPersistConsumerOffsetInterval(1000 * 5); + result.setPullTimeDelayMillsWhenException(1000); + result.setUnitMode(true); + result.setSocksProxyConfig("{}"); + result.setLanguage(LanguageCode.JAVA); + result.setDecodeReadBody(true); + result.setDecodeDecompressBody(true); + result.setAccessChannel(AccessChannel.LOCAL); + result.setMqClientApiTimeout(1000 * 3); + result.setEnableStreamRequestType(true); + result.setSendLatencyEnable(true); + result.setEnableHeartbeatChannelEventListener(true); + result.setDetectTimeout(200); + result.setDetectInterval(1000 * 2); + result.setUseHeartbeatV2(false); + result.buildMQClientId(); + result.setNamespace("lmq"); + return result; + } +} diff --git a/client/src/test/java/org/apache/rocketmq/client/ValidatorsTest.java b/client/src/test/java/org/apache/rocketmq/client/ValidatorsTest.java new file mode 100644 index 0000000..b00c5d1 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/ValidatorsTest.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.client; + +import java.util.Properties; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.exception.MQClientException; +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"); + Validators.checkTopic("%RETRY%Hello"); + Validators.checkTopic("_%RETRY%Hello"); + Validators.checkTopic("-%RETRY%Hello"); + Validators.checkTopic("223-%RETRY%Hello"); + } + + @Test + public void testCheckTopic_HasIllegalCharacters() { + String illegalTopic = "TOPIC&*^"; + try { + 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, "^[%|a-zA-Z0-9_-]+$")); + } + } + + @Test + public void testCheckTopic_BlankTopic() { + String blankTopic = ""; + try { + Validators.checkTopic(blankTopic); + failBecauseExceptionWasNotThrown(MQClientException.class); + } catch (MQClientException e) { + assertThat(e).hasMessageStartingWith("The specified topic is blank"); + } + } + + @Test + public void testCheckTopic_TooLongTopic() { + 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"); + } + } + + @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 new file mode 100644 index 0000000..87a71df --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/common/ThreadLocalIndexTest.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.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 testIncrementAndGet() throws Exception { + ThreadLocalIndex localIndex = new ThreadLocalIndex(); + int initialVal = localIndex.incrementAndGet(); + + 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 0000000..592c247 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/consumer/DefaultLitePullConsumerTest.java @@ -0,0 +1,902 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.Silent.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; + } + }); + + doAnswer(x -> new FindBrokerResult("127.0.0.1:10911", false)).when(mQClientFactory).findBrokerAddressInSubscribe(anyString(), anyLong(), anyBoolean()); + + 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 new file mode 100644 index 0000000..31788ac --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/consumer/DefaultMQPullConsumerTest.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.client.consumer; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.apache.rocketmq.client.ClientConfig; +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.PullAPIWrapper; +import org.apache.rocketmq.client.impl.consumer.PullResultExt; +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.remoting.protocol.header.PullMessageRequestHeader; +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.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.doAnswer; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class DefaultMQPullConsumerTest { + @Spy + private MQClientInstance mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(new ClientConfig()); + @Mock + private MQClientAPIImpl mQClientAPIImpl; + private DefaultMQPullConsumer pullConsumer; + private String consumerGroup = "FooBarGroup"; + private String topic = "FooBar"; + private String brokerName = "BrokerA"; + + @Before + public void init() throws Exception { + pullConsumer = new DefaultMQPullConsumer(consumerGroup); + pullConsumer.setNamesrvAddr("127.0.0.1:9876"); + pullConsumer.start(); + PullAPIWrapper pullAPIWrapper = pullConsumer.getDefaultMQPullConsumerImpl().getPullAPIWrapper(); + Field field = PullAPIWrapper.class.getDeclaredField("mQClientFactory"); + field.setAccessible(true); + field.set(pullAPIWrapper, mQClientFactory); + + field = MQClientInstance.class.getDeclaredField("mQClientAPIImpl"); + field.setAccessible(true); + field.set(mQClientFactory, mQClientAPIImpl); + + when(mQClientFactory.findBrokerAddressInSubscribe(anyString(), anyLong(), anyBoolean())).thenReturn(new FindBrokerResult("127.0.0.1:10911", false)); + } + + @After + public void terminate() { + pullConsumer.shutdown(); + } + + @Test + public void testStart_OffsetShouldNotNUllAfterStart() { + Assert.assertNotNull(pullConsumer.getOffsetStore()); + } + + @Test + public void testPullMessage_Success() throws Exception { + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock mock) throws Throwable { + PullMessageRequestHeader requestHeader = mock.getArgument(1); + return createPullResult(requestHeader, PullStatus.FOUND, Collections.singletonList(new MessageExt())); + } + }).when(mQClientAPIImpl).pullMessage(anyString(), any(PullMessageRequestHeader.class), anyLong(), any(CommunicationMode.class), nullable(PullCallback.class)); + + MessageQueue messageQueue = new MessageQueue(topic, brokerName, 0); + PullResult pullResult = pullConsumer.pull(messageQueue, "*", 1024, 3); + assertThat(pullResult).isNotNull(); + assertThat(pullResult.getPullStatus()).isEqualTo(PullStatus.FOUND); + assertThat(pullResult.getNextBeginOffset()).isEqualTo(1024 + 1); + assertThat(pullResult.getMinOffset()).isEqualTo(123); + assertThat(pullResult.getMaxOffset()).isEqualTo(2048); + assertThat(pullResult.getMsgFoundList()).isEqualTo(new ArrayList<>()); + } + + @Test + public void testPullMessage_NotFound() throws Exception { + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock mock) throws Throwable { + PullMessageRequestHeader requestHeader = mock.getArgument(1); + return createPullResult(requestHeader, PullStatus.NO_NEW_MSG, new ArrayList<>()); + } + }).when(mQClientAPIImpl).pullMessage(anyString(), any(PullMessageRequestHeader.class), anyLong(), any(CommunicationMode.class), nullable(PullCallback.class)); + + MessageQueue messageQueue = new MessageQueue(topic, brokerName, 0); + PullResult pullResult = pullConsumer.pull(messageQueue, "*", 1024, 3); + assertThat(pullResult.getPullStatus()).isEqualTo(PullStatus.NO_NEW_MSG); + } + + @Test + public void testPullMessageAsync_Success() throws Exception { + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock mock) throws Throwable { + PullMessageRequestHeader requestHeader = mock.getArgument(1); + PullResult pullResult = createPullResult(requestHeader, PullStatus.FOUND, Collections.singletonList(new MessageExt())); + + PullCallback pullCallback = mock.getArgument(4); + pullCallback.onSuccess(pullResult); + return null; + } + }).when(mQClientAPIImpl).pullMessage(anyString(), any(PullMessageRequestHeader.class), anyLong(), any(CommunicationMode.class), nullable(PullCallback.class)); + + MessageQueue messageQueue = new MessageQueue(topic, brokerName, 0); + pullConsumer.pull(messageQueue, "*", 1024, 3, new PullCallback() { + @Override + public void onSuccess(PullResult pullResult) { + assertThat(pullResult).isNotNull(); + assertThat(pullResult.getPullStatus()).isEqualTo(PullStatus.FOUND); + assertThat(pullResult.getNextBeginOffset()).isEqualTo(1024 + 1); + assertThat(pullResult.getMinOffset()).isEqualTo(123); + assertThat(pullResult.getMaxOffset()).isEqualTo(2048); + assertThat(pullResult.getMsgFoundList()).isEqualTo(new ArrayList<>()); + } + + @Override + public void onException(Throwable e) { + + } + }); + } + + private PullResultExt createPullResult(PullMessageRequestHeader requestHeader, PullStatus pullStatus, + List messageExtList) throws Exception { + return new PullResultExt(pullStatus, requestHeader.getQueueOffset() + messageExtList.size(), 123, 2048, messageExtList, 0, new byte[] {}); + } +} 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 new file mode 100644 index 0000000..834be5c --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/consumer/DefaultMQPushConsumerTest.java @@ -0,0 +1,419 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; +import org.apache.rocketmq.client.consumer.rebalance.AllocateMessageQueueAveragely; +import org.apache.rocketmq.client.consumer.rebalance.AllocateMessageQueueAveragelyByCircle; +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.ConsumeMessageOrderlyService; +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.RebalanceImpl; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.common.UtilAll; +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.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.junit.AfterClass; +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 java.io.ByteArrayOutputStream; +import java.lang.reflect.Field; +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.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 static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Fail.failBecauseExceptionWasNotThrown; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +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; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +@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 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 + 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(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, rebalanceImpl); + + field = DefaultMQPushConsumerImpl.class.getDeclaredField("doNotUpdateTopicSubscribeInfoWhenSubscriptionChanged"); + field.setAccessible(true); + field.set(null, true); + + Set messageQueueSet = new HashSet<>(); + messageQueueSet.add(createPullRequest().getMessageQueue()); + pushConsumerImpl.updateTopicSubscribeInfo(topic, messageQueueSet); + + pushConsumerImpl.setmQClientFactory(mQClientFactory); + + pullAPIWrapper = spy(new PullAPIWrapper(mQClientFactory, consumerGroup, false)); + FieldUtils.writeDeclaredField(pushConsumerImpl, "pullAPIWrapper", pullAPIWrapper, true); + + 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.setQueueOffset(queueOffset.getAndIncrement()); + messageClientExt.setMsgId("1024"); + messageClientExt.setBody(msgBody); + 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; + } + }); + + pushConsumer.subscribe(topic, "*"); + pushConsumer.start(); + + mQClientFactory.registerConsumer(consumerGroup, pushConsumerImpl); + } + + @AfterClass + public static void terminate() { + if (pushConsumer != null) { + pushConsumer.shutdown(); + } + } + + @Test + public void testStart_OffsetShouldNotNUllAfterStart() { + assertNotNull(pushConsumer.getOffsetStore()); + } + + @Test + public void testPullMessage_Success() throws InterruptedException, RemotingException, MQBrokerException { + 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(10, TimeUnit.SECONDS); + MessageExt msg = messageAtomic.get(); + assertThat(msg).isNotNull(); + assertThat(msg.getTopic()).isEqualTo(topic); + assertThat(msg.getBody()).isEqualTo(msgBody); + } + + @Test(timeout = 20000) + public void testPullMessage_SuccessWithOrderlyService() throws Exception { + final CountDownLatch countDownLatch = new CountDownLatch(1); + final AtomicReference messageAtomic = new AtomicReference<>(); + + MessageListenerOrderly listenerOrderly = new MessageListenerOrderly() { + @Override + public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) { + messageAtomic.set(msgs.get(0)); + countDownLatch.countDown(); + return null; + } + }; + pushConsumer.registerMessageListener(listenerOrderly); + pushConsumer.getDefaultMQPushConsumerImpl().setConsumeMessageService(new ConsumeMessageOrderlyService(pushConsumer.getDefaultMQPushConsumerImpl(), listenerOrderly)); + pushConsumer.getDefaultMQPushConsumerImpl().setConsumeOrderly(true); + pushConsumer.getDefaultMQPushConsumerImpl().doRebalance(); + PullMessageService pullMessageService = mQClientFactory.getPullMessageService(); + pullMessageService.executePullRequestLater(createPullRequest(), 100); + + countDownLatch.await(); + MessageExt msg = messageAtomic.get(); + assertThat(msg).isNotNull(); + assertThat(msg.getTopic()).isEqualTo(topic); + assertThat(msg.getBody()).isEqualTo(msgBody); + } + + @Test + public void testCheckConfig() { + DefaultMQPushConsumer pushConsumer = createPushConsumer(); + + pushConsumer.setPullThresholdForQueue(65535 + 1); + try { + pushConsumer.start(); + failBecauseExceptionWasNotThrown(MQClientException.class); + } catch (MQClientException e) { + assertThat(e).hasMessageContaining("pullThresholdForQueue Out of range [1, 65535]"); + } + + pushConsumer = createPushConsumer(); + pushConsumer.setPullThresholdForTopic(65535 * 100 + 1); + + try { + pushConsumer.start(); + failBecauseExceptionWasNotThrown(MQClientException.class); + } catch (MQClientException e) { + assertThat(e).hasMessageContaining("pullThresholdForTopic Out of range [1, 6553500]"); + } + + pushConsumer = createPushConsumer(); + pushConsumer.setPullThresholdSizeForQueue(1024 + 1); + try { + pushConsumer.start(); + failBecauseExceptionWasNotThrown(MQClientException.class); + } catch (MQClientException e) { + assertThat(e).hasMessageContaining("pullThresholdSizeForQueue Out of range [1, 1024]"); + } + + pushConsumer = createPushConsumer(); + pushConsumer.setPullThresholdSizeForTopic(1024 * 100 + 1); + try { + pushConsumer.start(); + failBecauseExceptionWasNotThrown(MQClientException.class); + } catch (MQClientException e) { + assertThat(e).hasMessageContaining("pullThresholdSizeForTopic Out of range [1, 102400]"); + } + } + + @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() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, + ConsumeConcurrentlyContext context) { + return null; + } + }); + return pushConsumer; + } + + private PullRequest createPullRequest() { + PullRequest pullRequest = new PullRequest(); + pullRequest.setConsumerGroup(consumerGroup); + pullRequest.setNextOffset(queueOffset.get()); + + 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 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(); + } + + @Test + public void assertCreatePushConsumer() { + DefaultMQPushConsumer pushConsumer1 = new DefaultMQPushConsumer(consumerGroup, mock(RPCHook.class)); + assertNotNull(pushConsumer1); + assertEquals(consumerGroup, pushConsumer1.getConsumerGroup()); + assertTrue(pushConsumer1.getAllocateMessageQueueStrategy() instanceof AllocateMessageQueueAveragely); + assertNotNull(pushConsumer1.defaultMQPushConsumerImpl); + assertFalse(pushConsumer1.isEnableTrace()); + assertTrue(UtilAll.isBlank(pushConsumer1.getTraceTopic())); + DefaultMQPushConsumer pushConsumer2 = new DefaultMQPushConsumer(consumerGroup, mock(RPCHook.class), new AllocateMessageQueueAveragelyByCircle()); + assertNotNull(pushConsumer2); + assertEquals(consumerGroup, pushConsumer2.getConsumerGroup()); + assertTrue(pushConsumer2.getAllocateMessageQueueStrategy() instanceof AllocateMessageQueueAveragelyByCircle); + assertNotNull(pushConsumer2.defaultMQPushConsumerImpl); + assertFalse(pushConsumer2.isEnableTrace()); + assertTrue(UtilAll.isBlank(pushConsumer2.getTraceTopic())); + } +} 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 0000000..6650f84 --- /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 0000000..ee314bc --- /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 0000000..498d9e2 --- /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 0000000..baae104 --- /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 0000000..62a7775 --- /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 new file mode 100644 index 0000000..261d6d6 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueConsitentHashTest.java @@ -0,0 +1,222 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.TreeMap; +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 AllocateMessageQueueConsitentHashTest { + + private String topic; + private static final String CID_PREFIX = "CID-"; + + @Before + public void init() { + topic = "topic_test"; + } + + @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); + Assert.assertEquals(result.size(), 0); + } + + @Test(expected = IllegalArgumentException.class) + public void testCurrentCIDIllegalArgument() { + List consumerIdList = createConsumerIdList(2); + List messageQueueList = createMessageQueueList(6); + new AllocateMessageQueueConsistentHash().allocate("", "", messageQueueList, consumerIdList); + } + + @Test(expected = IllegalArgumentException.class) + public void testMessageQueueIllegalArgument() { + String currentCID = "0"; + List consumerIdList = createConsumerIdList(2); + new AllocateMessageQueueConsistentHash().allocate("", currentCID, null, consumerIdList); + } + + @Test(expected = IllegalArgumentException.class) + public void testConsumerIdIllegalArgument() { + String currentCID = "0"; + List messageQueueList = createMessageQueueList(6); + new AllocateMessageQueueConsistentHash().allocate("", currentCID, messageQueueList, null); + } + + @Test + public void testAllocate1() { + testAllocate(20, 10); + } + + @Test + public void testAllocate2() { + testAllocate(10, 20); + } + + @Test + public void testRun100RandomCase() { + for (int i = 0; i < 10; i++) { + int consumerSize = new Random().nextInt(20) + 1;//1-20 + int queueSize = new Random().nextInt(20) + 1;//1-20 + testAllocate(queueSize, consumerSize); + try { + Thread.sleep(1); + } catch (InterruptedException e) { + } + } + } + + public void testAllocate(int queueSize, int consumerSize) { + AllocateMessageQueueStrategy allocateMessageQueueConsistentHash = new AllocateMessageQueueConsistentHash(3); + + List mqAll = createMessageQueueList(queueSize); + + List cidAll = createConsumerIdList(consumerSize); + List allocatedResAll = new ArrayList<>(); + + Map allocateToAllOrigin = new TreeMap<>(); + //test allocate all + { + + List cidBegin = new ArrayList<>(cidAll); + + for (String cid : cidBegin) { + List rs = allocateMessageQueueConsistentHash.allocate("testConsumerGroup", cid, mqAll, cidBegin); + for (MessageQueue mq : rs) { + allocateToAllOrigin.put(mq, cid); + } + allocatedResAll.addAll(rs); + } + + Assert.assertTrue( + verifyAllocateAll(cidBegin, mqAll, allocatedResAll)); + } + + Map allocateToAllAfterRemoveOne = new TreeMap<>(); + List cidAfterRemoveOne = new ArrayList<>(cidAll); + //test allocate remove one cid + { + String removeCID = cidAfterRemoveOne.remove(0); + List mqShouldOnlyChanged = new ArrayList<>(); + Iterator> it = allocateToAllOrigin.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + if (entry.getValue().equals(removeCID)) { + mqShouldOnlyChanged.add(entry.getKey()); + } + } + + 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); + } + } + + Assert.assertTrue("queueSize" + queueSize + "consumerSize:" + consumerSize + "\nmqAll:" + mqAll + "\nallocatedResAllAfterRemove" + allocatedResAllAfterRemove, + verifyAllocateAll(cidAfterRemoveOne, mqAll, allocatedResAllAfterRemove)); + verifyAfterRemove(allocateToAllOrigin, allocateToAllAfterRemoveOne, removeCID); + } + + List cidAfterAdd = new ArrayList<>(cidAfterRemoveOne); + //test allocate add one more cid + { + String newCid = CID_PREFIX + "NEW"; + cidAfterAdd.add(newCid); + 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); + for (MessageQueue mq : rs) { + allocateToAll3.put(mq, cid); + if (cid.equals(newCid)) { + mqShouldOnlyChanged.add(mq); + } + } + } + + Assert.assertTrue( + verifyAllocateAll(cidAfterAdd, mqAll, allocatedResAllAfterAdd)); + verifyAfterAdd(allocateToAllAfterRemoveOne, allocateToAll3, newCid); + } + } + + private boolean verifyAllocateAll(List cidAll, List mqAll, + List allocatedResAll) { + if (cidAll.isEmpty()) { + return allocatedResAll.isEmpty(); + } + return mqAll.containsAll(allocatedResAll) && allocatedResAll.containsAll(mqAll); + } + + private void verifyAfterRemove(Map allocateToBefore, Map allocateAfter, + String removeCID) { + for (MessageQueue mq : allocateToBefore.keySet()) { + String allocateToOrigin = allocateToBefore.get(mq); + if (allocateToOrigin.equals(removeCID)) { + + } else { //the rest queue should be the same + Assert.assertTrue(allocateAfter.get(mq).equals(allocateToOrigin));//should be the same + } + } + } + + private void verifyAfterAdd(Map allocateBefore, Map allocateAfter, + String newCID) { + for (MessageQueue mq : allocateAfter.keySet()) { + String allocateToOrigin = allocateBefore.get(mq); + String allocateToAfter = allocateAfter.get(mq); + if (allocateToAfter.equals(newCID)) { + + } else { //the rest queue should be the same + Assert.assertTrue("it was allocated to " + allocateToOrigin + ". Now, it is to " + allocateAfter.get(mq) + " mq:" + mq, allocateAfter.get(mq).equals(allocateToOrigin));//should be the same + } + } + } + + private List createConsumerIdList(int size) { + List consumerIdList = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + consumerIdList.add(CID_PREFIX + String.valueOf(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/store/ControllableOffsetTest.java b/client/src/test/java/org/apache/rocketmq/client/consumer/store/ControllableOffsetTest.java new file mode 100644 index 0000000..23a56ca --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/consumer/store/ControllableOffsetTest.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.client.consumer.store; + + +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +public class ControllableOffsetTest { + + private ControllableOffset controllableOffset; + + @Before + public void setUp() { + controllableOffset = new ControllableOffset(0); + } + + @Test + public void testUpdateAndFreeze_ShouldFreezeOffsetAtTargetValue() { + controllableOffset.updateAndFreeze(100); + assertEquals(100, controllableOffset.getOffset()); + controllableOffset.update(200); + assertEquals(100, controllableOffset.getOffset()); + } + + @Test + public void testUpdate_ShouldUpdateOffsetWhenNotFrozen() { + controllableOffset.update(200); + assertEquals(200, controllableOffset.getOffset()); + } + + @Test + public void testUpdate_ShouldNotUpdateOffsetWhenFrozen() { + controllableOffset.updateAndFreeze(100); + controllableOffset.update(200); + assertEquals(100, controllableOffset.getOffset()); + } + + @Test + public void testUpdate_ShouldNotDecreaseOffsetWhenIncreaseOnly() { + controllableOffset.update(200); + controllableOffset.update(100, true); + assertEquals(200, controllableOffset.getOffset()); + } + + @Test + public void testUpdate_ShouldUpdateOffsetToGreaterValueWhenIncreaseOnly() { + controllableOffset.update(100); + controllableOffset.update(200, true); + assertEquals(200, controllableOffset.getOffset()); + } + +} 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 new file mode 100644 index 0000000..2f88523 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/consumer/store/LocalFileOffsetStoreTest.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.client.consumer.store; + +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; + +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.common.message.MessageQueue; +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 LocalFileOffsetStoreTest { + @Mock + private MQClientInstance mQClientFactory; + private String group = "FooBarGroup"; + private String topic = "FooBar"; + private String brokerName = "DefaultBrokerName"; + + @Before + public void init() { + System.setProperty("rocketmq.client.localOffsetStoreDir", System.getProperty("java.io.tmpdir") + File.separator + ".rocketmq_offsets"); + String clientId = new ClientConfig().buildMQClientId() + "#TestNamespace" + System.currentTimeMillis(); + when(mQClientFactory.getClientId()).thenReturn(clientId); + } + + @Test + public void testUpdateOffset() throws Exception { + OffsetStore offsetStore = new LocalFileOffsetStore(mQClientFactory, group); + MessageQueue messageQueue = new MessageQueue(topic, brokerName, 1); + offsetStore.updateOffset(messageQueue, 1024, false); + + assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_MEMORY)).isEqualTo(1024); + + offsetStore.updateOffset(messageQueue, 1023, false); + assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_MEMORY)).isEqualTo(1023); + + offsetStore.updateOffset(messageQueue, 1022, true); + assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_MEMORY)).isEqualTo(1023); + } + + @Test + public void testReadOffset_FromStore() throws Exception { + OffsetStore offsetStore = new LocalFileOffsetStore(mQClientFactory, group); + MessageQueue messageQueue = new MessageQueue(topic, brokerName, 2); + + offsetStore.updateOffset(messageQueue, 1024, false); + assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_STORE)).isEqualTo(-1); + + offsetStore.persistAll(new HashSet<>(Collections.singletonList(messageQueue))); + assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_STORE)).isEqualTo(1024); + } + + @Test + public void testCloneOffset() throws Exception { + OffsetStore offsetStore = new LocalFileOffsetStore(mQClientFactory, group); + MessageQueue messageQueue = new MessageQueue(topic, brokerName, 3); + offsetStore.updateOffset(messageQueue, 1024, false); + Map cloneOffsetTable = offsetStore.cloneOffsetTable(topic); + + assertThat(cloneOffsetTable.size()).isEqualTo(1); + assertThat(cloneOffsetTable.get(messageQueue)).isEqualTo(1024); + } + + @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 new file mode 100644 index 0000000..ba6911e --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/consumer/store/RemoteBrokerOffsetStoreTest.java @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Collections; +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.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; +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.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class RemoteBrokerOffsetStoreTest { + @Mock + private MQClientInstance mQClientFactory; + @Mock + private MQClientAPIImpl mqClientAPI; + private String group = "FooBarGroup"; + private String topic = "FooBar"; + private String brokerName = "DefaultBrokerName"; + + @Before + 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.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 + public void testUpdateOffset() throws Exception { + OffsetStore offsetStore = new RemoteBrokerOffsetStore(mQClientFactory, group); + MessageQueue messageQueue = new MessageQueue(topic, brokerName, 1); + + offsetStore.updateOffset(messageQueue, 1024, false); + assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_MEMORY)).isEqualTo(1024); + + offsetStore.updateOffset(messageQueue, 1023, false); + assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_MEMORY)).isEqualTo(1023); + + offsetStore.updateOffset(messageQueue, 1022, true); + 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); + MessageQueue messageQueue = new MessageQueue(topic, brokerName, 2); + + offsetStore.updateOffset(messageQueue, 1024, false); + + 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); + } + + @Test + public void testReadOffset_Success() throws Exception { + OffsetStore offsetStore = new RemoteBrokerOffsetStore(mQClientFactory, group); + final MessageQueue messageQueue = new MessageQueue(topic, brokerName, 3); + + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock mock) throws Throwable { + UpdateConsumerOffsetRequestHeader updateRequestHeader = mock.getArgument(1); + when(mqClientAPI.queryConsumerOffset(anyString(), any(QueryConsumerOffsetRequestHeader.class), anyLong())).thenReturn(updateRequestHeader.getCommitOffset()); + return null; + } + }).when(mqClientAPI).updateConsumerOffsetOneway(any(String.class), any(UpdateConsumerOffsetRequestHeader.class), any(Long.class)); + + offsetStore.updateOffset(messageQueue, 1024, false); + offsetStore.persist(messageQueue); + assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_STORE)).isEqualTo(1024); + + offsetStore.updateOffset(messageQueue, 1023, false); + offsetStore.persist(messageQueue); + assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_STORE)).isEqualTo(1023); + + offsetStore.updateOffset(messageQueue, 1022, true); + offsetStore.persist(messageQueue); + assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_STORE)).isEqualTo(1023); + + offsetStore.updateOffset(messageQueue, 1025, false); + offsetStore.persistAll(new HashSet<>(Collections.singletonList(messageQueue))); + assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_STORE)).isEqualTo(1025); + } + + @Test + public void testRemoveOffset() throws Exception { + OffsetStore offsetStore = new RemoteBrokerOffsetStore(mQClientFactory, group); + final MessageQueue messageQueue = new MessageQueue(topic, brokerName, 4); + + 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/impl/ClientRemotingProcessorTest.java b/client/src/test/java/org/apache/rocketmq/client/impl/ClientRemotingProcessorTest.java new file mode 100644 index 0000000..ed31aa1 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/impl/ClientRemotingProcessorTest.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.client.impl; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.client.impl.producer.MQProducerInner; +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.remoting.exception.RemotingException; +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.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +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.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import static org.apache.rocketmq.common.message.MessageDecoder.NAME_VALUE_SEPARATOR; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ClientRemotingProcessorTest { + + @Mock + private MQClientInstance mQClientFactory; + + private ClientRemotingProcessor processor; + + private final String defaultTopic = "defaultTopic"; + + private final String defaultBroker = "defaultBroker"; + + private final String defaultGroup = "defaultGroup"; + + @Before + public void init() throws RemotingException, InterruptedException, MQClientException { + processor = new ClientRemotingProcessor(mQClientFactory); + ClientConfig clientConfig = mock(ClientConfig.class); + when(clientConfig.getNamespace()).thenReturn("namespace"); + when(mQClientFactory.getClientConfig()).thenReturn(clientConfig); + MQProducerInner producerInner = mock(MQProducerInner.class); + when(mQClientFactory.selectProducer(defaultGroup)).thenReturn(producerInner); + } + + @Test + public void testCheckTransactionState() throws Exception { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + RemotingCommand request = mock(RemotingCommand.class); + when(request.getCode()).thenReturn(RequestCode.CHECK_TRANSACTION_STATE); + when(request.getBody()).thenReturn(getMessageResult()); + CheckTransactionStateRequestHeader requestHeader = new CheckTransactionStateRequestHeader(); + when(request.decodeCommandCustomHeader(CheckTransactionStateRequestHeader.class)).thenReturn(requestHeader); + assertNull(processor.processRequest(ctx, request)); + } + + @Test + public void testNotifyConsumerIdsChanged() throws Exception { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + RemotingCommand request = mock(RemotingCommand.class); + when(request.getCode()).thenReturn(RequestCode.NOTIFY_CONSUMER_IDS_CHANGED); + NotifyConsumerIdsChangedRequestHeader requestHeader = new NotifyConsumerIdsChangedRequestHeader(); + when(request.decodeCommandCustomHeader(NotifyConsumerIdsChangedRequestHeader.class)).thenReturn(requestHeader); + assertNull(processor.processRequest(ctx, request)); + } + + @Test + public void testResetOffset() throws Exception { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + RemotingCommand request = mock(RemotingCommand.class); + when(request.getCode()).thenReturn(RequestCode.RESET_CONSUMER_CLIENT_OFFSET); + ResetOffsetBody offsetBody = new ResetOffsetBody(); + when(request.getBody()).thenReturn(RemotingSerializable.encode(offsetBody)); + ResetOffsetRequestHeader requestHeader = new ResetOffsetRequestHeader(); + when(request.decodeCommandCustomHeader(ResetOffsetRequestHeader.class)).thenReturn(requestHeader); + assertNull(processor.processRequest(ctx, request)); + } + + @Test + public void testGetConsumeStatus() throws Exception { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + RemotingCommand request = mock(RemotingCommand.class); + when(request.getCode()).thenReturn(RequestCode.GET_CONSUMER_STATUS_FROM_CLIENT); + GetConsumerStatusRequestHeader requestHeader = new GetConsumerStatusRequestHeader(); + when(request.decodeCommandCustomHeader(GetConsumerStatusRequestHeader.class)).thenReturn(requestHeader); + assertNotNull(processor.processRequest(ctx, request)); + } + + @Test + public void testGetConsumerRunningInfo() throws Exception { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + RemotingCommand request = mock(RemotingCommand.class); + when(request.getCode()).thenReturn(RequestCode.GET_CONSUMER_RUNNING_INFO); + ConsumerRunningInfo consumerRunningInfo = new ConsumerRunningInfo(); + consumerRunningInfo.setJstack("jstack"); + when(mQClientFactory.consumerRunningInfo(anyString())).thenReturn(consumerRunningInfo); + GetConsumerRunningInfoRequestHeader requestHeader = new GetConsumerRunningInfoRequestHeader(); + requestHeader.setJstackEnable(true); + requestHeader.setConsumerGroup(defaultGroup); + when(request.decodeCommandCustomHeader(GetConsumerRunningInfoRequestHeader.class)).thenReturn(requestHeader); + RemotingCommand command = processor.processRequest(ctx, request); + assertNotNull(command); + assertEquals(ResponseCode.SUCCESS, command.getCode()); + } + + @Test + public void testConsumeMessageDirectly() throws Exception { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + RemotingCommand request = mock(RemotingCommand.class); + when(request.getCode()).thenReturn(RequestCode.CONSUME_MESSAGE_DIRECTLY); + when(request.getBody()).thenReturn(getMessageResult()); + ConsumeMessageDirectlyResult directlyResult = mock(ConsumeMessageDirectlyResult.class); + when(mQClientFactory.consumeMessageDirectly(any(MessageExt.class), anyString(), anyString())).thenReturn(directlyResult); + ConsumeMessageDirectlyResultRequestHeader requestHeader = new ConsumeMessageDirectlyResultRequestHeader(); + requestHeader.setConsumerGroup(defaultGroup); + requestHeader.setBrokerName(defaultBroker); + when(request.decodeCommandCustomHeader(ConsumeMessageDirectlyResultRequestHeader.class)).thenReturn(requestHeader); + RemotingCommand command = processor.processRequest(ctx, request); + assertNotNull(command); + assertEquals(ResponseCode.SUCCESS, command.getCode()); + } + + @Test + public void testReceiveReplyMessage() throws Exception { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + RemotingCommand request = mock(RemotingCommand.class); + when(request.getCode()).thenReturn(RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT); + when(request.getBody()).thenReturn(getMessageResult()); + when(request.decodeCommandCustomHeader(ReplyMessageRequestHeader.class)).thenReturn(createReplyMessageRequestHeader()); + when(request.getBody()).thenReturn(new byte[1]); + RemotingCommand command = processor.processRequest(ctx, request); + assertNotNull(command); + assertEquals(ResponseCode.SUCCESS, command.getCode()); + } + + private ReplyMessageRequestHeader createReplyMessageRequestHeader() { + ReplyMessageRequestHeader result = new ReplyMessageRequestHeader(); + result.setTopic(defaultTopic); + result.setQueueId(0); + result.setStoreTimestamp(System.currentTimeMillis()); + result.setBornTimestamp(System.currentTimeMillis()); + result.setReconsumeTimes(1); + result.setBornHost("127.0.0.1:12911"); + result.setStoreHost("127.0.0.1:10911"); + result.setSysFlag(1); + result.setFlag(1); + result.setProperties("CORRELATION_ID" + NAME_VALUE_SEPARATOR + "1"); + return result; + } + + private byte[] getMessageResult() throws Exception { + byte[] bytes = MessageDecoder.encode(createMessageExt(), false); + ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length); + byteBuffer.put(bytes); + return byteBuffer.array(); + } + + private MessageExt createMessageExt() { + MessageExt result = new MessageExt(); + result.setBody("body".getBytes(StandardCharsets.UTF_8)); + result.setTopic(defaultTopic); + result.setBrokerName(defaultBroker); + result.putUserProperty("key", "value"); + result.getProperties().put(MessageConst.PROPERTY_PRODUCER_GROUP, defaultGroup); + result.getProperties().put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, "TX1"); + result.setKeys("keys"); + SocketAddress bornHost = new InetSocketAddress("127.0.0.1", 12911); + SocketAddress storeHost = new InetSocketAddress("127.0.0.1", 10911); + result.setBornHost(bornHost); + result.setStoreHost(storeHost); + return result; + } +} diff --git a/client/src/test/java/org/apache/rocketmq/client/impl/MQAdminImplTest.java b/client/src/test/java/org/apache/rocketmq/client/impl/MQAdminImplTest.java new file mode 100644 index 0000000..f52aba2 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/impl/MQAdminImplTest.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.client.impl; + +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.QueryResult; +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.common.MixAll; +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.sysflag.MessageSysFlag; +import org.apache.rocketmq.remoting.InvokeCallback; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.QueryMessageResponseHeader; +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.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Set; + +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.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class MQAdminImplTest { + + @Mock + private MQClientInstance mQClientFactory; + + @Mock + private MQClientAPIImpl mQClientAPIImpl; + + private MQAdminImpl mqAdminImpl; + + private final String defaultTopic = "defaultTopic"; + + private final String defaultBroker = "defaultBroker"; + + private final String defaultCluster = "defaultCluster"; + + private final String defaultBrokerAddr = "127.0.0.1:10911"; + + private final long defaultTimeout = 3000L; + + @Before + public void init() throws RemotingException, InterruptedException, MQClientException { + when(mQClientFactory.getMQClientAPIImpl()).thenReturn(mQClientAPIImpl); + when(mQClientAPIImpl.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(createRouteData()); + ClientConfig clientConfig = mock(ClientConfig.class); + when(clientConfig.getNamespace()).thenReturn("namespace"); + when(mQClientFactory.getClientConfig()).thenReturn(clientConfig); + when(mQClientFactory.findBrokerAddressInPublish(any())).thenReturn(defaultBrokerAddr); + when(mQClientFactory.getAnExistTopicRouteData(any())).thenReturn(createRouteData()); + mqAdminImpl = new MQAdminImpl(mQClientFactory); + } + + @Test + public void assertTimeoutMillis() { + assertEquals(6000L, mqAdminImpl.getTimeoutMillis()); + mqAdminImpl.setTimeoutMillis(defaultTimeout); + assertEquals(defaultTimeout, mqAdminImpl.getTimeoutMillis()); + } + + @Test + public void testCreateTopic() throws MQClientException { + mqAdminImpl.createTopic("", defaultTopic, 6); + } + + @Test + public void assertFetchPublishMessageQueues() throws MQClientException { + List queueList = mqAdminImpl.fetchPublishMessageQueues(defaultTopic); + assertNotNull(queueList); + assertEquals(6, queueList.size()); + for (MessageQueue each : queueList) { + assertEquals(defaultTopic, each.getTopic()); + assertEquals(defaultBroker, each.getBrokerName()); + } + } + + @Test + public void assertFetchSubscribeMessageQueues() throws MQClientException { + Set queueList = mqAdminImpl.fetchSubscribeMessageQueues(defaultTopic); + assertNotNull(queueList); + assertEquals(6, queueList.size()); + for (MessageQueue each : queueList) { + assertEquals(defaultTopic, each.getTopic()); + assertEquals(defaultBroker, each.getBrokerName()); + } + } + + @Test + public void assertSearchOffset() throws MQClientException { + assertEquals(0, mqAdminImpl.searchOffset(new MessageQueue(), defaultTimeout)); + } + + @Test + public void assertMaxOffset() throws MQClientException { + assertEquals(0, mqAdminImpl.maxOffset(new MessageQueue())); + } + + @Test + public void assertMinOffset() throws MQClientException { + assertEquals(0, mqAdminImpl.minOffset(new MessageQueue())); + } + + @Test + public void assertEarliestMsgStoreTime() throws MQClientException { + assertEquals(0, mqAdminImpl.earliestMsgStoreTime(new MessageQueue())); + } + + @Test(expected = MQClientException.class) + public void assertViewMessage() throws MQBrokerException, RemotingException, InterruptedException, MQClientException { + MessageExt actual = mqAdminImpl.viewMessage(defaultTopic, "1"); + assertNotNull(actual); + } + + @Test + public void assertQueryMessage() throws InterruptedException, MQClientException, MQBrokerException, RemotingException { + doAnswer(invocation -> { + InvokeCallback callback = invocation.getArgument(3); + QueryMessageResponseHeader responseHeader = new QueryMessageResponseHeader(); + responseHeader.setIndexLastUpdatePhyoffset(1L); + responseHeader.setIndexLastUpdateTimestamp(System.currentTimeMillis()); + RemotingCommand response = mock(RemotingCommand.class); + when(response.decodeCommandCustomHeader(QueryMessageResponseHeader.class)).thenReturn(responseHeader); + when(response.getBody()).thenReturn(getMessageResult()); + when(response.getCode()).thenReturn(ResponseCode.SUCCESS); + callback.operationSucceed(response); + return null; + }).when(mQClientAPIImpl).queryMessage(anyString(), any(), anyLong(), any(InvokeCallback.class), any()); + QueryResult actual = mqAdminImpl.queryMessage(defaultTopic, "keys", 100, 1L, 50L); + assertNotNull(actual); + assertEquals(1, actual.getMessageList().size()); + assertEquals(defaultTopic, actual.getMessageList().get(0).getTopic()); + } + + @Test + public void assertQueryMessageByUniqKey() throws InterruptedException, MQClientException, MQBrokerException, RemotingException { + doAnswer(invocation -> { + InvokeCallback callback = invocation.getArgument(3); + QueryMessageResponseHeader responseHeader = new QueryMessageResponseHeader(); + responseHeader.setIndexLastUpdatePhyoffset(1L); + responseHeader.setIndexLastUpdateTimestamp(System.currentTimeMillis()); + RemotingCommand response = mock(RemotingCommand.class); + when(response.decodeCommandCustomHeader(QueryMessageResponseHeader.class)).thenReturn(responseHeader); + when(response.getBody()).thenReturn(getMessageResult()); + when(response.getCode()).thenReturn(ResponseCode.SUCCESS); + callback.operationSucceed(response); + return null; + }).when(mQClientAPIImpl).queryMessage(anyString(), any(), anyLong(), any(InvokeCallback.class), any()); + String msgId = buildMsgId(); + MessageExt actual = mqAdminImpl.queryMessageByUniqKey(defaultTopic, msgId); + assertNotNull(actual); + assertEquals(msgId, actual.getMsgId()); + assertEquals(defaultTopic, actual.getTopic()); + actual = mqAdminImpl.queryMessageByUniqKey(defaultCluster, defaultTopic, msgId); + assertNotNull(actual); + assertEquals(msgId, actual.getMsgId()); + assertEquals(defaultTopic, actual.getTopic()); + QueryResult queryResult = mqAdminImpl.queryMessageByUniqKey(defaultTopic, msgId, 1, 0L, 1L); + assertNotNull(queryResult); + assertEquals(1, queryResult.getMessageList().size()); + assertEquals(defaultTopic, queryResult.getMessageList().get(0).getTopic()); + } + + private String buildMsgId() { + MessageExt msgExt = createMessageExt(); + int storeHostIPLength = (msgExt.getFlag() & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0 ? 4 : 16; + int msgIDLength = storeHostIPLength + 4 + 8; + ByteBuffer byteBufferMsgId = ByteBuffer.allocate(msgIDLength); + return MessageDecoder.createMessageId(byteBufferMsgId, msgExt.getStoreHostBytes(), msgExt.getCommitLogOffset()); + } + + private TopicRouteData createRouteData() { + TopicRouteData result = new TopicRouteData(); + result.setBrokerDatas(createBrokerData()); + result.setQueueDatas(createQueueData()); + return result; + } + + private List createBrokerData() { + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(MixAll.MASTER_ID, defaultBrokerAddr); + return Collections.singletonList(new BrokerData(defaultCluster, defaultBroker, brokerAddrs)); + } + + private List createQueueData() { + QueueData queueData = new QueueData(); + queueData.setPerm(6); + queueData.setBrokerName(defaultBroker); + queueData.setReadQueueNums(6); + queueData.setWriteQueueNums(6); + return Collections.singletonList(queueData); + } + + private byte[] getMessageResult() throws Exception { + byte[] bytes = MessageDecoder.encode(createMessageExt(), false); + ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length); + byteBuffer.put(bytes); + return byteBuffer.array(); + } + + private MessageExt createMessageExt() { + MessageExt result = new MessageExt(); + result.setBody("body".getBytes(StandardCharsets.UTF_8)); + result.setTopic(defaultTopic); + result.setBrokerName(defaultBroker); + result.putUserProperty("key", "value"); + result.setKeys("keys"); + SocketAddress bornHost = new InetSocketAddress("127.0.0.1", 12911); + SocketAddress storeHost = new InetSocketAddress("127.0.0.1", 10911); + result.setBornHost(bornHost); + result.setStoreHost(storeHost); + return result; + } +} 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 new file mode 100644 index 0000000..c12b23c --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/impl/MQClientAPIImplTest.java @@ -0,0 +1,2080 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; + +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.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.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.ObjectCreator; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +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.MessageQueue; +import org.apache.rocketmq.common.message.MessageQueueAssignment; +import org.apache.rocketmq.common.message.MessageRequestMode; +import org.apache.rocketmq.common.namesrv.TopAddressing; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.InvokeCallback; +import org.apache.rocketmq.remoting.RemotingClient; +import org.apache.rocketmq.remoting.common.HeartbeatV2Result; +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.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyRemotingClient; +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.ConsumeStats; +import org.apache.rocketmq.remoting.protocol.admin.TopicOffset; +import org.apache.rocketmq.remoting.protocol.admin.TopicStatsTable; +import org.apache.rocketmq.remoting.protocol.body.AclInfo; +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.BrokerStatsItem; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.body.Connection; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +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.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.ProducerConnection; +import org.apache.rocketmq.remoting.protocol.body.ProducerInfo; +import org.apache.rocketmq.remoting.protocol.body.ProducerTableInfo; +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.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.body.UserInfo; +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.EndTransactionRequestHeader; +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.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PullMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumerOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.RecallMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.RecallMessageResponseHeader; +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.UpdateGroupForbiddenRequestHeader; +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.namesrv.AddWritePermOfBrokerResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.GetKVConfigResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.WipeWritePermOfBrokerResponseHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +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.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +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.statictopic.TopicQueueMappingInfo; +import org.apache.rocketmq.remoting.protocol.subscription.GroupForbidden; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.assertj.core.api.Assertions; +import org.junit.Assert; +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 org.mockito.stubbing.Answer; + +import java.io.UnsupportedEncodingException; +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.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.CountDownLatch; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Fail.failBecauseExceptionWasNotThrown; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +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.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class MQClientAPIImplTest { + + private MQClientAPIImpl mqClientAPI = new MQClientAPIImpl(new NettyClientConfig(), null, null, new ClientConfig()); + + @Mock + private RemotingClient remotingClient; + + @Mock + private DefaultMQProducerImpl defaultMQProducerImpl; + + @Mock + private RemotingCommand response; + + private final String brokerAddr = "127.0.0.1"; + + private final String brokerName = "DefaultBroker"; + + private final String clusterName = "DefaultCluster"; + + private final String group = "FooBarGroup"; + + private final String topic = "FooBar"; + + private final Message msg = new Message("FooBar", new byte[]{}); + + private final String clientId = "127.0.0.2@UnitTest"; + + private final String defaultTopic = "defaultTopic"; + + private final String defaultBrokerAddr = "127.0.0.1:10911"; + + private final String defaultNsAddr = "127.0.0.1:9876"; + + private final long defaultTimeout = 3000L; + + @Before + public void init() throws Exception { + Field field = MQClientAPIImpl.class.getDeclaredField("remotingClient"); + field.setAccessible(true); + field.set(mqClientAPI, remotingClient); + } + + @Test + public void testSendMessageOneWay_Success() throws RemotingException, InterruptedException, MQBrokerException { + doNothing().when(remotingClient).invokeOneway(anyString(), any(RemotingCommand.class), anyLong()); + SendResult sendResult = mqClientAPI.sendMessage(brokerAddr, brokerName, msg, new SendMessageRequestHeader(), + 3 * 1000, CommunicationMode.ONEWAY, new SendMessageContext(), defaultMQProducerImpl); + assertThat(sendResult).isNull(); + } + + @Test + public void testSendMessageOneWay_WithException() throws RemotingException, InterruptedException, MQBrokerException { + doThrow(new RemotingTimeoutException("Remoting Exception in Test")).when(remotingClient).invokeOneway(anyString(), any(RemotingCommand.class), anyLong()); + try { + mqClientAPI.sendMessage(brokerAddr, brokerName, msg, new SendMessageRequestHeader(), + 3 * 1000, CommunicationMode.ONEWAY, new SendMessageContext(), defaultMQProducerImpl); + failBecauseExceptionWasNotThrown(RemotingException.class); + } catch (RemotingException e) { + assertThat(e).hasMessage("Remoting Exception in Test"); + } + + doThrow(new InterruptedException("Interrupted Exception in Test")).when(remotingClient).invokeOneway(anyString(), any(RemotingCommand.class), anyLong()); + try { + mqClientAPI.sendMessage(brokerAddr, brokerName, msg, new SendMessageRequestHeader(), + 3 * 1000, CommunicationMode.ONEWAY, new SendMessageContext(), defaultMQProducerImpl); + failBecauseExceptionWasNotThrown(InterruptedException.class); + } catch (InterruptedException e) { + assertThat(e).hasMessage("Interrupted Exception in Test"); + } + } + + @Test + public void testSendMessageSync_Success() throws InterruptedException, RemotingException, MQBrokerException { + doAnswer(mock -> { + RemotingCommand request = mock.getArgument(1); + return createSendMessageSuccessResponse(request); + }).when(remotingClient).invokeSync(anyString(), any(RemotingCommand.class), anyLong()); + + SendMessageRequestHeader requestHeader = createSendMessageRequestHeader(); + + SendResult sendResult = mqClientAPI.sendMessage(brokerAddr, brokerName, msg, requestHeader, + 3 * 1000, CommunicationMode.SYNC, new SendMessageContext(), defaultMQProducerImpl); + + assertThat(sendResult.getSendStatus()).isEqualTo(SendStatus.SEND_OK); + assertThat(sendResult.getOffsetMsgId()).isEqualTo("123"); + assertThat(sendResult.getQueueOffset()).isEqualTo(123L); + assertThat(sendResult.getMessageQueue().getQueueId()).isEqualTo(1); + } + + @Test + public void testSendMessageSync_WithException() throws InterruptedException, RemotingException { + doAnswer(mock -> { + RemotingCommand request = mock.getArgument(1); + RemotingCommand response = RemotingCommand.createResponseCommand(SendMessageResponseHeader.class); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setOpaque(request.getOpaque()); + response.setRemark("Broker is broken."); + return response; + }).when(remotingClient).invokeSync(anyString(), any(RemotingCommand.class), anyLong()); + + SendMessageRequestHeader requestHeader = createSendMessageRequestHeader(); + + try { + mqClientAPI.sendMessage(brokerAddr, brokerName, msg, requestHeader, + 3 * 1000, CommunicationMode.SYNC, new SendMessageContext(), defaultMQProducerImpl); + failBecauseExceptionWasNotThrown(MQBrokerException.class); + } catch (MQBrokerException e) { + assertThat(e).hasMessageContaining("Broker is broken."); + } + } + + @Test + public void testSendMessageAsync_Success() throws RemotingException, InterruptedException, MQBrokerException { + doNothing().when(remotingClient).invokeAsync(anyString(), any(RemotingCommand.class), anyLong(), any(InvokeCallback.class)); + SendResult sendResult = mqClientAPI.sendMessage(brokerAddr, brokerName, msg, new SendMessageRequestHeader(), + 3 * 1000, CommunicationMode.ASYNC, new SendMessageContext(), defaultMQProducerImpl); + assertThat(sendResult).isNull(); + + doAnswer(mock -> { + 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(anyString(), any(RemotingCommand.class), anyLong(), any(InvokeCallback.class)); + 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) { + 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 testSendMessageAsync_WithException() throws RemotingException, InterruptedException, MQBrokerException { + doThrow(new RemotingTimeoutException("Remoting Exception in Test")).when(remotingClient) + .invokeAsync(anyString(), any(RemotingCommand.class), anyLong(), any(InvokeCallback.class)); + 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 testResumeCheckHalfMessage_WithException() throws RemotingException, InterruptedException { + doAnswer(mock -> { + 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, "topic,", "test", 3000); + assertThat(result).isEqualTo(false); + } + + @Test + public void testResumeCheckHalfMessage_Success() throws InterruptedException, RemotingException { + doAnswer(mock -> { + RemotingCommand request = mock.getArgument(1); + return createResumeSuccessResponse(request); + }).when(remotingClient).invokeSync(anyString(), any(RemotingCommand.class), anyLong()); + + boolean result = mqClientAPI.resumeCheckHalfMessage(brokerAddr, "topic", "test", 3000); + + assertThat(result).isEqualTo(true); + } + + @Test + public void testSendMessageTypeofReply() throws Exception { + doAnswer(mock -> { + 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(ArgumentMatchers.anyString(), ArgumentMatchers.any(RemotingCommand.class), ArgumentMatchers.anyLong(), ArgumentMatchers.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((Answer) 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((Answer) mock -> { + 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((Answer) mock -> { + 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 testPopMultiLmqMessage_async() throws Exception { + final long popTime = System.currentTimeMillis(); + final int invisibleTime = 10 * 1000; + final String lmqTopic = MixAll.LMQ_PREFIX + "lmq1"; + final String lmqTopic2 = MixAll.LMQ_PREFIX + "lmq2"; + final String multiDispatch = String.join(MixAll.LMQ_DISPATCH_SEPARATOR, lmqTopic, lmqTopic2); + final String multiOffset = String.join(MixAll.LMQ_DISPATCH_SEPARATOR, "0", "0"); + doAnswer((Answer) mock -> { + 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(0); + message.setQueueOffset(10L); + message.setCommitLogOffset(10000L); + 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_INNER_MULTI_DISPATCH, multiDispatch); + MessageAccessor.putProperty(message, MessageConst.PROPERTY_INNER_MULTI_QUEUE_OFFSET, multiOffset); + 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(multiDispatch); + assertThat(popResult.getMsgFoundList().get(0).getProperty(MessageConst.PROPERTY_INNER_MULTI_QUEUE_OFFSET)) + .isEqualTo(multiOffset); + 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((Answer) mock -> { + 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((Answer) mock -> { + 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((Answer) 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((Answer) 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((Answer) 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((Answer) mock -> { + 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, "topic", 100L, 10000); + assertThat(messageExt.getTopic()).isEqualTo(topic); + } + + @Test + public void testSearchOffset() throws Exception { + doAnswer((Answer) 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((Answer) 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((Answer) 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((Answer) 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((Answer) 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((Answer) 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((Answer) 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()); + + SendMessageResponseHeader responseHeader = (SendMessageResponseHeader) response.readCustomHeader(); + responseHeader.setMsgId("123"); + responseHeader.setQueueId(1); + responseHeader.setQueueOffset(123L); + + response.addExtField(MessageConst.PROPERTY_MSG_REGION, "RegionHZ"); + response.addExtField(MessageConst.PROPERTY_TRACE_SWITCH, "true"); + response.addExtField("queueId", String.valueOf(responseHeader.getQueueId())); + response.addExtField("msgId", responseHeader.getMsgId()); + response.addExtField("queueOffset", String.valueOf(responseHeader.getQueueOffset())); + 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 SendMessageRequestHeader createSendMessageRequestHeader() { + SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); + requestHeader.setBornTimestamp(System.currentTimeMillis()); + requestHeader.setTopic(topic); + requestHeader.setProducerGroup(group); + requestHeader.setQueueId(1); + requestHeader.setMaxReconsumeTimes(10); + return requestHeader; + } + + @Test + public void testAddWritePermOfBroker() throws Exception { + doAnswer(invocationOnMock -> { + 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); + } + + @Test + public void testCreateTopicList_Success() throws RemotingException, InterruptedException, MQClientException { + doAnswer((Answer) 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()); + + final List topicConfigList = new LinkedList<>(); + for (int i = 0; i < 16; i++) { + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName("Topic" + i); + topicConfigList.add(topicConfig); + } + + mqClientAPI.createTopicList(brokerAddr, topicConfigList, 10000); + } + + @Test + public void assertFetchNameServerAddr() throws NoSuchFieldException, IllegalAccessException { + setTopAddressing(); + assertEquals(defaultNsAddr, mqClientAPI.fetchNameServerAddr()); + } + + @Test + public void assertOnNameServerAddressChange() { + assertEquals(defaultNsAddr, mqClientAPI.onNameServerAddressChange(defaultNsAddr)); + } + + @Test + public void assertPullMessage() throws MQBrokerException, RemotingException, InterruptedException { + PullMessageRequestHeader requestHeader = mock(PullMessageRequestHeader.class); + mockInvokeSync(); + PullCallback callback = mock(PullCallback.class); + PullMessageResponseHeader responseHeader = mock(PullMessageResponseHeader.class); + setResponseHeader(responseHeader); + when(responseHeader.getNextBeginOffset()).thenReturn(1L); + when(responseHeader.getMinOffset()).thenReturn(1L); + when(responseHeader.getMaxOffset()).thenReturn(10L); + when(responseHeader.getSuggestWhichBrokerId()).thenReturn(MixAll.MASTER_ID); + PullResult actual = mqClientAPI.pullMessage(defaultBrokerAddr, requestHeader, defaultTimeout, CommunicationMode.SYNC, callback); + assertNotNull(actual); + assertEquals(1L, actual.getNextBeginOffset()); + assertEquals(1L, actual.getMinOffset()); + assertEquals(10L, actual.getMaxOffset()); + assertEquals(PullStatus.FOUND, actual.getPullStatus()); + assertNull(actual.getMsgFoundList()); + } + + @Test + public void testBatchAckMessageAsync() throws MQBrokerException, RemotingException, InterruptedException { + AckCallback callback = mock(AckCallback.class); + List extraInfoList = new ArrayList<>(); + extraInfoList.add(String.format("%s %s %s %s %s %s %d %d", "1", "2", "3", "4", "5", brokerName, 7, 8)); + mqClientAPI.batchAckMessageAsync(defaultBrokerAddr, defaultTimeout, callback, defaultTopic, "", extraInfoList); + } + + @Test + public void assertSearchOffset() throws MQBrokerException, RemotingException, InterruptedException { + mockInvokeSync(); + SearchOffsetResponseHeader responseHeader = mock(SearchOffsetResponseHeader.class); + when(responseHeader.getOffset()).thenReturn(1L); + setResponseHeader(responseHeader); + assertEquals(1L, mqClientAPI.searchOffset(defaultBrokerAddr, new MessageQueue(), System.currentTimeMillis(), defaultTimeout)); + } + + @Test + public void testUpdateConsumerOffsetOneway() throws RemotingException, InterruptedException { + UpdateConsumerOffsetRequestHeader requestHeader = mock(UpdateConsumerOffsetRequestHeader.class); + mqClientAPI.updateConsumerOffsetOneway(defaultBrokerAddr, requestHeader, defaultTimeout); + } + + @Test + public void assertSendHeartbeat() throws MQBrokerException, RemotingException, InterruptedException { + mockInvokeSync(); + HeartbeatData heartbeatData = new HeartbeatData(); + assertEquals(1, mqClientAPI.sendHeartbeat(defaultBrokerAddr, heartbeatData, defaultTimeout)); + } + + @Test + public void assertSendHeartbeatV2() throws MQBrokerException, RemotingException, InterruptedException { + mockInvokeSync(); + HeartbeatData heartbeatData = new HeartbeatData(); + HeartbeatV2Result actual = mqClientAPI.sendHeartbeatV2(defaultBrokerAddr, heartbeatData, defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.getVersion()); + assertFalse(actual.isSubChange()); + assertFalse(actual.isSupportV2()); + } + + @Test + public void testUnregisterClient() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + mqClientAPI.unregisterClient(defaultBrokerAddr, "", "", "", defaultTimeout); + } + + @Test + public void testEndTransactionOneway() throws RemotingException, InterruptedException { + mockInvokeSync(); + EndTransactionRequestHeader requestHeader = mock(EndTransactionRequestHeader.class); + mqClientAPI.endTransactionOneway(defaultBrokerAddr, requestHeader, "", defaultTimeout); + } + + @Test + public void testQueryMessage() throws MQBrokerException, RemotingException, InterruptedException { + QueryMessageRequestHeader requestHeader = mock(QueryMessageRequestHeader.class); + InvokeCallback callback = mock(InvokeCallback.class); + mqClientAPI.queryMessage(defaultBrokerAddr, requestHeader, defaultTimeout, callback, false); + } + + @Test + public void testRegisterClient() throws RemotingException, InterruptedException { + mockInvokeSync(); + HeartbeatData heartbeatData = new HeartbeatData(); + assertTrue(mqClientAPI.registerClient(defaultBrokerAddr, heartbeatData, defaultTimeout)); + } + + @Test + public void testConsumerSendMessageBack() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + MessageExt messageExt = mock(MessageExt.class); + mqClientAPI.consumerSendMessageBack(defaultBrokerAddr, brokerName, messageExt, "", 1, defaultTimeout, 1000); + } + + @Test + public void assertLockBatchMQ() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + LockBatchRequestBody responseBody = new LockBatchRequestBody(); + setResponseBody(responseBody); + Set actual = mqClientAPI.lockBatchMQ(defaultBrokerAddr, responseBody, defaultTimeout); + assertNotNull(actual); + assertEquals(0, actual.size()); + } + + @Test + public void testUnlockBatchMQ() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + UnlockBatchRequestBody unlockBatchRequestBody = new UnlockBatchRequestBody(); + mqClientAPI.unlockBatchMQ(defaultBrokerAddr, unlockBatchRequestBody, defaultTimeout, false); + } + + @Test + public void assertGetTopicStatsInfo() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + TopicStatsTable responseBody = new TopicStatsTable(); + MessageQueue messageQueue = new MessageQueue(); + TopicOffset topicOffset = new TopicOffset(); + responseBody.getOffsetTable().put(messageQueue, topicOffset); + setResponseBody(responseBody); + TopicStatsTable actual = mqClientAPI.getTopicStatsInfo(defaultBrokerAddr, defaultTopic, defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.getOffsetTable().size()); + } + + @Test + public void assertGetConsumeStats() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + ConsumeStats responseBody = new ConsumeStats(); + responseBody.setConsumeTps(1000); + setResponseBody(responseBody); + ConsumeStats actual = mqClientAPI.getConsumeStats(defaultBrokerAddr, "", defaultTimeout); + assertNotNull(actual); + assertEquals(1000, actual.getConsumeTps(), 0.0); + } + + @Test + public void assertGetProducerConnectionList() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + ProducerConnection responseBody = new ProducerConnection(); + responseBody.getConnectionSet().add(new Connection()); + setResponseBody(responseBody); + ProducerConnection actual = mqClientAPI.getProducerConnectionList(defaultBrokerAddr, "", defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.getConnectionSet().size()); + } + + @Test + public void assertGetAllProducerInfo() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + Map> data = new HashMap<>(); + data.put("key", Collections.emptyList()); + ProducerTableInfo responseBody = new ProducerTableInfo(data); + setResponseBody(responseBody); + ProducerTableInfo actual = mqClientAPI.getAllProducerInfo(defaultBrokerAddr, defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.getData().size()); + } + + @Test + public void assertGetConsumerConnectionList() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + ConsumerConnection responseBody = new ConsumerConnection(); + responseBody.setConsumeType(ConsumeType.CONSUME_ACTIVELY); + responseBody.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); + responseBody.setMessageModel(MessageModel.CLUSTERING); + setResponseBody(responseBody); + ConsumerConnection actual = mqClientAPI.getConsumerConnectionList(defaultBrokerAddr, "", defaultTimeout); + assertNotNull(actual); + assertEquals(ConsumeType.CONSUME_ACTIVELY, actual.getConsumeType()); + assertEquals(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET, actual.getConsumeFromWhere()); + assertEquals(MessageModel.CLUSTERING, actual.getMessageModel()); + } + + @Test + public void assertGetBrokerRuntimeInfo() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + KVTable responseBody = new KVTable(); + responseBody.getTable().put("key", "value"); + setResponseBody(responseBody); + KVTable actual = mqClientAPI.getBrokerRuntimeInfo(defaultBrokerAddr, defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.getTable().size()); + } + + @Test + public void testAddBroker() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + mqClientAPI.addBroker(defaultBrokerAddr, "", defaultTimeout); + } + + @Test + public void testRemoveBroker() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + mqClientAPI.removeBroker(defaultBrokerAddr, clusterName, brokerName, MixAll.MASTER_ID, defaultTimeout); + } + + @Test + public void testUpdateBrokerConfig() throws RemotingException, InterruptedException, MQBrokerException, UnsupportedEncodingException, MQClientException { + mockInvokeSync(); + mqClientAPI.updateBrokerConfig(defaultBrokerAddr, createProperties(), defaultTimeout); + } + + @Test + public void assertGetBrokerConfig() throws RemotingException, InterruptedException, MQBrokerException, UnsupportedEncodingException { + mockInvokeSync(); + setResponseBody("{\"key\":\"value\"}"); + Properties actual = mqClientAPI.getBrokerConfig(defaultBrokerAddr, defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.size()); + } + + @Test + public void testUpdateColdDataFlowCtrGroupConfig() throws RemotingException, InterruptedException, MQBrokerException, UnsupportedEncodingException { + mockInvokeSync(); + Properties props = new Properties(); + mqClientAPI.updateColdDataFlowCtrGroupConfig(defaultBrokerAddr, props, defaultTimeout); + } + + @Test + public void testRemoveColdDataFlowCtrGroupConfig() throws RemotingException, InterruptedException, MQBrokerException, UnsupportedEncodingException { + mockInvokeSync(); + mqClientAPI.removeColdDataFlowCtrGroupConfig(defaultBrokerAddr, "", defaultTimeout); + } + + @Test + public void assertGetColdDataFlowCtrInfo() throws RemotingException, InterruptedException, MQBrokerException, UnsupportedEncodingException { + mockInvokeSync(); + setResponseBody("{\"key\":\"value\"}"); + String actual = mqClientAPI.getColdDataFlowCtrInfo(defaultBrokerAddr, defaultTimeout); + assertNotNull(actual); + assertEquals("\"{\\\"key\\\":\\\"value\\\"}\"", actual); + } + + @Test + public void assertSetCommitLogReadAheadMode() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + when(response.getRemark()).thenReturn("remark"); + String actual = mqClientAPI.setCommitLogReadAheadMode(defaultBrokerAddr, "", defaultTimeout); + assertNotNull(actual); + assertEquals("remark", actual); + } + + @Test + public void assertGetBrokerClusterInfo() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + ClusterInfo responseBody = new ClusterInfo(); + Map> clusterAddrTable = new HashMap<>(); + clusterAddrTable.put(clusterName, new HashSet<>()); + Map brokerAddrTable = new HashMap<>(); + brokerAddrTable.put(brokerName, new BrokerData()); + responseBody.setClusterAddrTable(clusterAddrTable); + responseBody.setBrokerAddrTable(brokerAddrTable); + setResponseBody(responseBody); + ClusterInfo actual = mqClientAPI.getBrokerClusterInfo(defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.getClusterAddrTable().size()); + assertEquals(1, actual.getBrokerAddrTable().size()); + } + + @Test + public void assertGetDefaultTopicRouteInfoFromNameServer() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + TopicRouteData responseBody = new TopicRouteData(); + responseBody.getQueueDatas().add(new QueueData()); + responseBody.getBrokerDatas().add(new BrokerData()); + responseBody.getFilterServerTable().put("key", Collections.emptyList()); + Map topicQueueMappingByBroker = new HashMap<>(); + topicQueueMappingByBroker.put("key", new TopicQueueMappingInfo()); + responseBody.setTopicQueueMappingByBroker(topicQueueMappingByBroker); + setResponseBody(responseBody); + TopicRouteData actual = mqClientAPI.getDefaultTopicRouteInfoFromNameServer(defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.getQueueDatas().size()); + assertEquals(1, actual.getBrokerDatas().size()); + assertEquals(1, actual.getFilterServerTable().size()); + assertEquals(1, actual.getTopicQueueMappingByBroker().size()); + } + + @Test + public void assertGetTopicRouteInfoFromNameServer() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + TopicRouteData responseBody = new TopicRouteData(); + responseBody.getQueueDatas().add(new QueueData()); + responseBody.getBrokerDatas().add(new BrokerData()); + responseBody.getFilterServerTable().put("key", Collections.emptyList()); + Map topicQueueMappingByBroker = new HashMap<>(); + topicQueueMappingByBroker.put("key", new TopicQueueMappingInfo()); + responseBody.setTopicQueueMappingByBroker(topicQueueMappingByBroker); + setResponseBody(responseBody); + TopicRouteData actual = mqClientAPI.getTopicRouteInfoFromNameServer(defaultTopic, defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.getQueueDatas().size()); + assertEquals(1, actual.getBrokerDatas().size()); + assertEquals(1, actual.getFilterServerTable().size()); + assertEquals(1, actual.getTopicQueueMappingByBroker().size()); + } + + @Test + public void assertGetTopicListFromNameServer() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + TopicList responseBody = new TopicList(); + responseBody.setBrokerAddr(defaultBrokerAddr); + responseBody.getTopicList().add(defaultTopic); + setResponseBody(responseBody); + TopicList actual = mqClientAPI.getTopicListFromNameServer(defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.getTopicList().size()); + assertEquals(defaultBrokerAddr, actual.getBrokerAddr()); + } + + @Test + public void assertWipeWritePermOfBroker() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + WipeWritePermOfBrokerResponseHeader responseHeader = mock(WipeWritePermOfBrokerResponseHeader.class); + when(responseHeader.getWipeTopicCount()).thenReturn(1); + setResponseHeader(responseHeader); + assertEquals(1, mqClientAPI.wipeWritePermOfBroker(defaultNsAddr, brokerName, defaultTimeout)); + } + + @Test + public void testDeleteTopicInBroker() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + mqClientAPI.deleteTopicInBroker(defaultBrokerAddr, defaultTopic, defaultTimeout); + } + + @Test + public void testDeleteTopicInNameServer() throws RemotingException, InterruptedException, MQClientException, MQBrokerException { + mockInvokeSync(); + mqClientAPI.deleteTopicInNameServer(defaultNsAddr, defaultTopic, defaultTimeout); + mqClientAPI.deleteTopicInNameServer(defaultNsAddr, clusterName, defaultTopic, defaultTimeout); + } + + @Test + public void testDeleteSubscriptionGroup() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + mqClientAPI.deleteSubscriptionGroup(defaultBrokerAddr, "", true, defaultTimeout); + } + + @Test + public void assertGetKVConfigValue() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + GetKVConfigResponseHeader responseHeader = mock(GetKVConfigResponseHeader.class); + when(responseHeader.getValue()).thenReturn("value"); + setResponseHeader(responseHeader); + assertEquals("value", mqClientAPI.getKVConfigValue("", "", defaultTimeout)); + } + + @Test + public void testPutKVConfigValue() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + mqClientAPI.putKVConfigValue("", "", "", defaultTimeout); + } + + @Test + public void testDeleteKVConfigValue() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + mqClientAPI.deleteKVConfigValue("", "", defaultTimeout); + } + + @Test + public void assertGetKVListByNamespace() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + KVTable responseBody = new KVTable(); + responseBody.getTable().put("key", "value"); + setResponseBody(responseBody); + KVTable actual = mqClientAPI.getKVListByNamespace("", defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.getTable().size()); + } + + @Test + public void assertInvokeBrokerToResetOffset() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + ResetOffsetBody responseBody = new ResetOffsetBody(); + responseBody.getOffsetTable().put(new MessageQueue(), 1L); + setResponseBody(responseBody); + Map actual = mqClientAPI.invokeBrokerToResetOffset(defaultBrokerAddr, defaultTopic, "", System.currentTimeMillis(), false, defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.size()); + actual = mqClientAPI.invokeBrokerToResetOffset(defaultBrokerAddr, defaultTopic, "", System.currentTimeMillis(), 1, 1L, defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.size()); + } + + @Test + public void assertInvokeBrokerToGetConsumerStatus() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + GetConsumerStatusBody responseBody = new GetConsumerStatusBody(); + responseBody.getConsumerTable().put("key", new HashMap<>()); + responseBody.getMessageQueueTable().put(new MessageQueue(), 1L); + setResponseBody(responseBody); + Map> actual = mqClientAPI.invokeBrokerToGetConsumerStatus(defaultBrokerAddr, defaultTopic, "", "", defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.size()); + } + + @Test + public void assertQueryTopicConsumeByWho() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + GroupList responseBody = new GroupList(); + responseBody.getGroupList().add(""); + setResponseBody(responseBody); + GroupList actual = mqClientAPI.queryTopicConsumeByWho(defaultBrokerAddr, defaultTopic, defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.getGroupList().size()); + } + + @Test + public void assertQueryTopicsByConsumer() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + TopicList responseBody = new TopicList(); + responseBody.getTopicList().add(defaultTopic); + responseBody.setBrokerAddr(defaultBrokerAddr); + setResponseBody(responseBody); + TopicList actual = mqClientAPI.queryTopicsByConsumer(defaultBrokerAddr, "", defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.getTopicList().size()); + assertEquals(defaultBrokerAddr, actual.getBrokerAddr()); + } + + @Test + public void assertQuerySubscriptionByConsumer() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + QuerySubscriptionResponseBody responseBody = new QuerySubscriptionResponseBody(); + SubscriptionData subscriptionData = new SubscriptionData(); + subscriptionData.setTopic(defaultTopic); + responseBody.setSubscriptionData(subscriptionData); + setResponseBody(responseBody); + SubscriptionData actual = mqClientAPI.querySubscriptionByConsumer(defaultBrokerAddr, group, defaultTopic, defaultTimeout); + assertNotNull(actual); + assertEquals(defaultTopic, actual.getTopic()); + } + + @Test + public void assertQueryConsumeTimeSpan() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + QueryConsumeTimeSpanBody responseBody = new QueryConsumeTimeSpanBody(); + responseBody.getConsumeTimeSpanSet().add(new QueueTimeSpan()); + setResponseBody(responseBody); + List actual = mqClientAPI.queryConsumeTimeSpan(defaultBrokerAddr, defaultTopic, group, defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.size()); + } + + @Test + public void assertGetTopicsByCluster() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + TopicList responseBody = new TopicList(); + responseBody.setBrokerAddr(defaultBrokerAddr); + responseBody.setTopicList(Collections.singleton(defaultTopic)); + setResponseBody(responseBody); + TopicList actual = mqClientAPI.getTopicsByCluster(clusterName, defaultTimeout); + assertNotNull(actual); + assertEquals(defaultBrokerAddr, actual.getBrokerAddr()); + assertEquals(1, actual.getTopicList().size()); + assertTrue(actual.getTopicList().contains(defaultTopic)); + } + + @Test + public void assertGetSystemTopicList() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + TopicList responseBody = new TopicList(); + responseBody.setBrokerAddr(defaultBrokerAddr); + responseBody.setTopicList(Collections.singleton(defaultTopic)); + setResponseBody(responseBody); + TopicList actual = mqClientAPI.getSystemTopicList(defaultTimeout); + assertNotNull(actual); + assertEquals(defaultBrokerAddr, actual.getBrokerAddr()); + assertEquals(1, actual.getTopicList().size()); + assertTrue(actual.getTopicList().contains(defaultTopic)); + } + + @Test + public void assertGetSystemTopicListFromBroker() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + TopicList responseBody = new TopicList(); + responseBody.setBrokerAddr(defaultBrokerAddr); + responseBody.setTopicList(Collections.singleton(defaultTopic)); + setResponseBody(responseBody); + TopicList actual = mqClientAPI.getSystemTopicListFromBroker(defaultBrokerAddr, defaultTimeout); + assertNotNull(actual); + assertEquals(defaultBrokerAddr, actual.getBrokerAddr()); + assertEquals(1, actual.getTopicList().size()); + assertTrue(actual.getTopicList().contains(defaultTopic)); + } + + @Test + public void assertCleanExpiredConsumeQueue() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + assertTrue(mqClientAPI.cleanExpiredConsumeQueue(defaultBrokerAddr, defaultTimeout)); + } + + @Test + public void assertDeleteExpiredCommitLog() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + assertTrue(mqClientAPI.deleteExpiredCommitLog(defaultBrokerAddr, defaultTimeout)); + } + + @Test + public void assertCleanUnusedTopicByAddr() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + assertTrue(mqClientAPI.cleanUnusedTopicByAddr(defaultBrokerAddr, defaultTimeout)); + } + + @Test + public void assertGetConsumerRunningInfo() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + ConsumerRunningInfo responseBody = new ConsumerRunningInfo(); + responseBody.setJstack("jstack"); + responseBody.getUserConsumerInfo().put("key", "value"); + setResponseBody(responseBody); + ConsumerRunningInfo actual = mqClientAPI.getConsumerRunningInfo(defaultBrokerAddr, group, clientId, false, defaultTimeout); + assertNotNull(actual); + assertEquals("jstack", actual.getJstack()); + assertEquals(1, actual.getUserConsumerInfo().size()); + } + + @Test + public void assertConsumeMessageDirectly() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + ConsumeMessageDirectlyResult responseBody = new ConsumeMessageDirectlyResult(); + responseBody.setAutoCommit(true); + responseBody.setRemark("remark"); + setResponseBody(responseBody); + ConsumeMessageDirectlyResult actual = mqClientAPI.consumeMessageDirectly(defaultBrokerAddr, group, clientId, topic, "", defaultTimeout); + assertNotNull(actual); + assertEquals("remark", actual.getRemark()); + assertTrue(actual.isAutoCommit()); + } + + @Test + public void assertQueryCorrectionOffset() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + QueryCorrectionOffsetBody responseBody = new QueryCorrectionOffsetBody(); + responseBody.getCorrectionOffsets().put(1, 1L); + setResponseBody(responseBody); + Map actual = mqClientAPI.queryCorrectionOffset(defaultBrokerAddr, topic, group, new HashSet<>(), defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.size()); + assertTrue(actual.containsKey(1)); + assertTrue(actual.containsValue(1L)); + } + + @Test + public void assertGetUnitTopicList() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + TopicList responseBody = new TopicList(); + responseBody.getTopicList().add(defaultTopic); + setResponseBody(responseBody); + TopicList actual = mqClientAPI.getUnitTopicList(false, defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.getTopicList().size()); + } + + @Test + public void assertGetHasUnitSubTopicList() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + TopicList responseBody = new TopicList(); + responseBody.getTopicList().add(defaultTopic); + setResponseBody(responseBody); + TopicList actual = mqClientAPI.getHasUnitSubTopicList(false, defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.getTopicList().size()); + } + + @Test + public void assertGetHasUnitSubUnUnitTopicList() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + TopicList responseBody = new TopicList(); + responseBody.getTopicList().add(defaultTopic); + setResponseBody(responseBody); + TopicList actual = mqClientAPI.getHasUnitSubUnUnitTopicList(false, defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.getTopicList().size()); + } + + @Test + public void testCloneGroupOffset() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + mqClientAPI.cloneGroupOffset(defaultBrokerAddr, "", "", defaultTopic, false, defaultTimeout); + } + + @Test + public void assertViewBrokerStatsData() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + BrokerStatsData responseBody = new BrokerStatsData(); + responseBody.setStatsDay(new BrokerStatsItem()); + setResponseBody(responseBody); + BrokerStatsData actual = mqClientAPI.viewBrokerStatsData(defaultBrokerAddr, "", "", defaultTimeout); + assertNotNull(actual); + assertNotNull(actual.getStatsDay()); + } + + @Test + public void assertGetClusterList() { + Set actual = mqClientAPI.getClusterList(topic, defaultTimeout); + assertNotNull(actual); + assertEquals(0, actual.size()); + } + + @Test + public void assertFetchConsumeStatsInBroker() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + ConsumeStatsList responseBody = new ConsumeStatsList(); + responseBody.setBrokerAddr(defaultBrokerAddr); + responseBody.getConsumeStatsList().add(new HashMap<>()); + setResponseBody(responseBody); + ConsumeStatsList actual = mqClientAPI.fetchConsumeStatsInBroker(defaultBrokerAddr, false, defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.getConsumeStatsList().size()); + assertEquals(defaultBrokerAddr, actual.getBrokerAddr()); + } + + @Test + public void assertGetAllSubscriptionGroupForSubscriptionGroupWrapper() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + SubscriptionGroupWrapper responseBody = new SubscriptionGroupWrapper(); + responseBody.getSubscriptionGroupTable().put("key", new SubscriptionGroupConfig()); + setResponseBody(responseBody); + SubscriptionGroupWrapper actual = mqClientAPI.getAllSubscriptionGroup(defaultBrokerAddr, defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.getSubscriptionGroupTable().size()); + assertNotNull(actual.getDataVersion()); + assertEquals(0, actual.getDataVersion().getStateVersion()); + } + + @Test + public void assertGetAllSubscriptionGroupForSubscriptionGroupConfig() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + SubscriptionGroupConfig responseBody = new SubscriptionGroupConfig(); + responseBody.setGroupName(group); + responseBody.setBrokerId(MixAll.MASTER_ID); + setResponseBody(responseBody); + SubscriptionGroupConfig actual = mqClientAPI.getSubscriptionGroupConfig(defaultBrokerAddr, group, defaultTimeout); + assertNotNull(actual); + assertEquals(group, actual.getGroupName()); + assertEquals(MixAll.MASTER_ID, actual.getBrokerId()); + } + + @Test + public void assertGetAllTopicConfig() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + TopicConfigSerializeWrapper responseBody = new TopicConfigSerializeWrapper(); + responseBody.getTopicConfigTable().put("key", new TopicConfig()); + setResponseBody(responseBody); + TopicConfigSerializeWrapper actual = mqClientAPI.getAllTopicConfig(defaultBrokerAddr, defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.getTopicConfigTable().size()); + assertNotNull(actual.getDataVersion()); + assertEquals(0, actual.getDataVersion().getStateVersion()); + } + + @Test + public void testUpdateNameServerConfig() throws RemotingException, InterruptedException, MQClientException, UnsupportedEncodingException { + mockInvokeSync(); + mqClientAPI.updateNameServerConfig(createProperties(), Collections.singletonList(defaultNsAddr), defaultTimeout); + } + + @Test + public void assertGetNameServerConfig() throws RemotingException, InterruptedException, UnsupportedEncodingException, MQClientException { + mockInvokeSync(); + setResponseBody("{\"key\":\"value\"}"); + Map actual = mqClientAPI.getNameServerConfig(Collections.singletonList(defaultNsAddr), defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.size()); + assertTrue(actual.containsKey(defaultNsAddr)); + } + + @Test + public void assertQueryConsumeQueue() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + QueryConsumeQueueResponseBody responseBody = new QueryConsumeQueueResponseBody(); + responseBody.setQueueData(Collections.singletonList(new ConsumeQueueData())); + setResponseBody(responseBody); + QueryConsumeQueueResponseBody actual = mqClientAPI.queryConsumeQueue(defaultBrokerAddr, defaultTopic, 1, 1, 1, group, defaultTimeout); + assertNotNull(actual); + assertEquals(1, actual.getQueueData().size()); + } + + @Test + public void testCheckClientInBroker() throws RemotingException, InterruptedException, MQClientException { + mockInvokeSync(); + mqClientAPI.checkClientInBroker(defaultBrokerAddr, group, clientId, new SubscriptionData(), defaultTimeout); + } + + @Test + public void assertGetTopicConfig() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + TopicConfigAndQueueMapping responseBody = new TopicConfigAndQueueMapping(new TopicConfig(), new TopicQueueMappingDetail()); + setResponseBody(responseBody); + TopicConfigAndQueueMapping actual = mqClientAPI.getTopicConfig(defaultBrokerAddr, defaultTopic, defaultTimeout); + assertNotNull(actual); + assertNotNull(actual.getMappingDetail()); + } + + @Test + public void testCreateStaticTopic() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + mqClientAPI.createStaticTopic(defaultBrokerAddr, defaultTopic, new TopicConfig(), new TopicQueueMappingDetail(), false, defaultTimeout); + } + + @Test + public void assertUpdateAndGetGroupForbidden() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + GroupForbidden responseBody = new GroupForbidden(); + responseBody.setGroup(group); + responseBody.setTopic(defaultTopic); + setResponseBody(responseBody); + GroupForbidden actual = mqClientAPI.updateAndGetGroupForbidden(defaultBrokerAddr, new UpdateGroupForbiddenRequestHeader(), defaultTimeout); + assertNotNull(actual); + assertEquals(group, actual.getGroup()); + assertEquals(defaultTopic, actual.getTopic()); + } + + @Test + public void testResetMasterFlushOffset() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + mqClientAPI.resetMasterFlushOffset(defaultBrokerAddr, 1L); + } + + @Test + public void assertGetBrokerHAStatus() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + HARuntimeInfo responseBody = new HARuntimeInfo(); + responseBody.setMaster(true); + responseBody.setMasterCommitLogMaxOffset(1L); + setResponseBody(responseBody); + HARuntimeInfo actual = mqClientAPI.getBrokerHAStatus(defaultBrokerAddr, defaultTimeout); + assertNotNull(actual); + assertEquals(1L, actual.getMasterCommitLogMaxOffset()); + assertTrue(actual.isMaster()); + } + + @Test + public void assertGetControllerMetaData() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + GetMetaDataResponseHeader responseHeader = new GetMetaDataResponseHeader(); + responseHeader.setGroup(group); + responseHeader.setIsLeader(true); + setResponseHeader(responseHeader); + GetMetaDataResponseHeader actual = mqClientAPI.getControllerMetaData(defaultBrokerAddr); + assertNotNull(actual); + assertEquals(group, actual.getGroup()); + assertTrue(actual.isLeader()); + } + + @Test + public void assertGetInSyncStateData() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + BrokerReplicasInfo responseBody = new BrokerReplicasInfo(); + BrokerReplicasInfo.ReplicasInfo replicasInfo = new BrokerReplicasInfo.ReplicasInfo(MixAll.MASTER_ID, defaultBrokerAddr, 1, 1, Collections.emptyList(), Collections.emptyList()); + responseBody.getReplicasInfoTable().put("key", replicasInfo); + GetMetaDataResponseHeader responseHeader = new GetMetaDataResponseHeader(); + responseHeader.setControllerLeaderAddress(defaultBrokerAddr); + setResponseHeader(responseHeader); + setResponseBody(responseBody); + BrokerReplicasInfo actual = mqClientAPI.getInSyncStateData(defaultBrokerAddr, Collections.singletonList(defaultBrokerAddr)); + assertNotNull(actual); + assertEquals(1L, actual.getReplicasInfoTable().size()); + } + + @Test + public void assertGetBrokerEpochCache() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + EpochEntryCache responseBody = new EpochEntryCache(clusterName, brokerName, MixAll.MASTER_ID, Collections.emptyList(), 1); + setResponseBody(responseBody); + EpochEntryCache actual = mqClientAPI.getBrokerEpochCache(defaultBrokerAddr); + assertNotNull(actual); + assertEquals(1L, actual.getMaxOffset()); + assertEquals(MixAll.MASTER_ID, actual.getBrokerId()); + assertEquals(brokerName, actual.getBrokerName()); + assertEquals(clusterName, actual.getClusterName()); + } + + @Test + public void assertGetControllerConfig() throws RemotingException, InterruptedException, UnsupportedEncodingException, MQClientException { + mockInvokeSync(); + setResponseBody("{\"key\":\"value\"}"); + Map actual = mqClientAPI.getControllerConfig(Collections.singletonList(defaultBrokerAddr), defaultTimeout); + assertNotNull(actual); + assertEquals(1L, actual.size()); + } + + @Test + public void testUpdateControllerConfig() throws RemotingException, InterruptedException, UnsupportedEncodingException, MQClientException { + mockInvokeSync(); + mqClientAPI.updateControllerConfig(createProperties(), Collections.singletonList(defaultBrokerAddr), defaultTimeout); + } + + @Test + public void assertElectMaster() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + BrokerMemberGroup responseBody = new BrokerMemberGroup(); + setResponseBody(responseBody); + GetMetaDataResponseHeader getMetaDataResponseHeader = new GetMetaDataResponseHeader(); + getMetaDataResponseHeader.setControllerLeaderAddress(defaultBrokerAddr); + when(response.decodeCommandCustomHeader(GetMetaDataResponseHeader.class)).thenReturn(getMetaDataResponseHeader); + ElectMasterResponseHeader responseHeader = new ElectMasterResponseHeader(); + when(response.decodeCommandCustomHeader(ElectMasterResponseHeader.class)).thenReturn(responseHeader); + Pair actual = mqClientAPI.electMaster(defaultBrokerAddr, clusterName, brokerName, MixAll.MASTER_ID); + assertNotNull(actual); + assertEquals(responseHeader, actual.getObject1()); + assertEquals(responseBody, actual.getObject2()); + } + + @Test + public void testCleanControllerBrokerData() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + GetMetaDataResponseHeader responseHeader = new GetMetaDataResponseHeader(); + responseHeader.setControllerLeaderAddress(defaultBrokerAddr); + setResponseHeader(responseHeader); + mqClientAPI.cleanControllerBrokerData(defaultBrokerAddr, clusterName, brokerName, "", false); + } + + @Test + public void testCreateUser() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + mqClientAPI.createUser(defaultBrokerAddr, new UserInfo(), defaultTimeout); + } + + @Test + public void testUpdateUser() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + mqClientAPI.updateUser(defaultBrokerAddr, new UserInfo(), defaultTimeout); + } + + @Test + public void testDeleteUser() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + mqClientAPI.deleteUser(defaultBrokerAddr, "", defaultTimeout); + } + + @Test + public void assertGetUser() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + setResponseBody(createUserInfo()); + UserInfo actual = mqClientAPI.getUser(defaultBrokerAddr, "", defaultTimeout); + assertNotNull(actual); + assertEquals("username", actual.getUsername()); + assertEquals("password", actual.getPassword()); + assertEquals("userStatus", actual.getUserStatus()); + assertEquals("userType", actual.getUserType()); + } + + @Test + public void assertListUser() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + setResponseBody(Collections.singletonList(createUserInfo())); + List actual = mqClientAPI.listUser(defaultBrokerAddr, "", defaultTimeout); + assertNotNull(actual); + assertEquals("username", actual.get(0).getUsername()); + assertEquals("password", actual.get(0).getPassword()); + assertEquals("userStatus", actual.get(0).getUserStatus()); + assertEquals("userType", actual.get(0).getUserType()); + } + + @Test + public void testCreateAcl() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + mqClientAPI.createAcl(defaultBrokerAddr, new AclInfo(), defaultTimeout); + } + + @Test + public void testUpdateAcl() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + mqClientAPI.updateAcl(defaultBrokerAddr, new AclInfo(), defaultTimeout); + } + + @Test + public void testDeleteAcl() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + mqClientAPI.deleteAcl(defaultBrokerAddr, "", "", defaultTimeout); + } + + @Test + public void assertGetAcl() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + setResponseBody(createAclInfo()); + AclInfo actual = mqClientAPI.getAcl(defaultBrokerAddr, "", defaultTimeout); + assertNotNull(actual); + assertEquals("subject", actual.getSubject()); + assertEquals(1, actual.getPolicies().size()); + } + + @Test + public void assertListAcl() throws RemotingException, InterruptedException, MQBrokerException { + mockInvokeSync(); + setResponseBody(Collections.singletonList(createAclInfo())); + List actual = mqClientAPI.listAcl(defaultBrokerAddr, "", "", defaultTimeout); + assertNotNull(actual); + assertEquals("subject", actual.get(0).getSubject()); + assertEquals(1, actual.get(0).getPolicies().size()); + } + + @Test + public void testRecallMessage() throws RemotingException, InterruptedException, MQBrokerException { + RecallMessageRequestHeader requestHeader = new RecallMessageRequestHeader(); + requestHeader.setProducerGroup(group); + requestHeader.setTopic(topic); + requestHeader.setRecallHandle("handle"); + requestHeader.setBrokerName(brokerName); + + // success + mockInvokeSync(); + String msgId = MessageClientIDSetter.createUniqID(); + RecallMessageResponseHeader responseHeader = new RecallMessageResponseHeader(); + responseHeader.setMsgId(msgId); + setResponseHeader(responseHeader); + String result = mqClientAPI.recallMessage(defaultBrokerAddr, requestHeader, defaultTimeout); + assertEquals(msgId, result); + + // error + when(response.getCode()).thenReturn(ResponseCode.SYSTEM_ERROR); + when(response.getRemark()).thenReturn("error"); + MQBrokerException e = assertThrows(MQBrokerException.class, () -> { + mqClientAPI.recallMessage(defaultBrokerAddr, requestHeader, defaultTimeout); + }); + assertEquals(ResponseCode.SYSTEM_ERROR, e.getResponseCode()); + assertEquals("error", e.getErrorMessage()); + assertEquals(defaultBrokerAddr, e.getBrokerAddr()); + } + + @Test + public void testRecallMessageAsync() throws RemotingException, InterruptedException { + RecallMessageRequestHeader requestHeader = new RecallMessageRequestHeader(); + requestHeader.setProducerGroup(group); + requestHeader.setTopic(topic); + requestHeader.setRecallHandle("handle"); + requestHeader.setBrokerName(brokerName); + String msgId = "msgId"; + doAnswer((Answer) mock -> { + InvokeCallback callback = mock.getArgument(3); + RemotingCommand request = mock.getArgument(1); + RemotingCommand response = RemotingCommand.createResponseCommand(RecallMessageResponseHeader.class); + response.setCode(ResponseCode.SUCCESS); + response.setOpaque(request.getOpaque()); + RecallMessageResponseHeader responseHeader = (RecallMessageResponseHeader) response.readCustomHeader(); + responseHeader.setMsgId(msgId); + ResponseFuture responseFuture = new ResponseFuture(null, request.getOpaque(), 3 * 1000, null, null); + 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.recallMessageAsync(defaultBrokerAddr, requestHeader, + defaultTimeout, new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + } + + @Override + public void operationSucceed(RemotingCommand response) { + RecallMessageResponseHeader responseHeader = (RecallMessageResponseHeader) response.readCustomHeader(); + Assert.assertEquals(msgId, responseHeader.getMsgId()); + done.countDown(); + } + + @Override + public void operationFail(Throwable throwable) { + } + }); + done.await(); + } + + @Test + public void testMQClientAPIImplWithoutObjectCreator() { + MQClientAPIImpl clientAPI = new MQClientAPIImpl( + new NettyClientConfig(), + null, + null, + new ClientConfig(), + null, + null + ); + RemotingClient remotingClient1 = clientAPI.getRemotingClient(); + Assert.assertTrue(remotingClient1 instanceof NettyRemotingClient); + } + + @Test + public void testMQClientAPIImplWithObjectCreator() { + ObjectCreator clientObjectCreator = args -> new MockRemotingClientTest((NettyClientConfig) args[0]); + final NettyClientConfig nettyClientConfig = new NettyClientConfig(); + MQClientAPIImpl clientAPI = new MQClientAPIImpl( + nettyClientConfig, + null, + null, + new ClientConfig(), + null, + clientObjectCreator + ); + RemotingClient remotingClient1 = clientAPI.getRemotingClient(); + Assert.assertTrue(remotingClient1 instanceof MockRemotingClientTest); + MockRemotingClientTest remotingClientTest = (MockRemotingClientTest) remotingClient1; + Assert.assertSame(remotingClientTest.getNettyClientConfig(), nettyClientConfig); + } + + private static class MockRemotingClientTest extends NettyRemotingClient { + public MockRemotingClientTest(NettyClientConfig nettyClientConfig) { + super(nettyClientConfig); + } + + public NettyClientConfig getNettyClientConfig() { + return nettyClientConfig; + } + } + + private Properties createProperties() { + Properties result = new Properties(); + result.put("key", "value"); + return result; + } + + private AclInfo createAclInfo() { + return AclInfo.of("subject", Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), ""); + } + + private UserInfo createUserInfo() { + UserInfo result = new UserInfo(); + result.setUsername("username"); + result.setPassword("password"); + result.setUserStatus("userStatus"); + result.setUserType("userType"); + return result; + } + + private void setResponseHeader(CommandCustomHeader responseHeader) throws RemotingCommandException { + when(response.decodeCommandCustomHeader(any())).thenReturn(responseHeader); + } + + private void setResponseBody(Object responseBody) { + when(response.getBody()).thenReturn(RemotingSerializable.encode(responseBody)); + } + + private void mockInvokeSync() throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException { + when(response.getCode()).thenReturn(ResponseCode.SUCCESS); + when(response.getVersion()).thenReturn(1); + when(remotingClient.invokeSync(any(), any(), anyLong())).thenReturn(response); + when(remotingClient.getNameServerAddressList()).thenReturn(Collections.singletonList(defaultNsAddr)); + } + + private void setTopAddressing() throws NoSuchFieldException, IllegalAccessException { + TopAddressing topAddressing = mock(TopAddressing.class); + setField(mqClientAPI, "topAddressing", topAddressing); + when(topAddressing.fetchNSAddr()).thenReturn(defaultNsAddr); + } + + private void setField(final Object target, final String fieldName, final Object newValue) throws NoSuchFieldException, IllegalAccessException { + Class clazz = target.getClass(); + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, newValue); + } +} diff --git a/client/src/test/java/org/apache/rocketmq/client/impl/admin/MqClientAdminImplTest.java b/client/src/test/java/org/apache/rocketmq/client/impl/admin/MqClientAdminImplTest.java new file mode 100644 index 0000000..71682fb --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/impl/admin/MqClientAdminImplTest.java @@ -0,0 +1,559 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.rocketmq.client.exception.MQClientException; +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.remoting.RemotingClient; +import org.apache.rocketmq.remoting.exception.RemotingException; +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.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.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; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +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 MqClientAdminImplTest { + + @Mock + private RemotingClient remotingClient; + + @Mock + private RemotingCommand response; + + private MqClientAdminImpl mqClientAdminImpl; + + private final String defaultTopic = "defaultTopic"; + + private final String defaultBrokerAddr = "127.0.0.1:10911"; + + private final long defaultTimeout = 3000L; + + @Before + public void init() throws RemotingException, InterruptedException, MQClientException { + mqClientAdminImpl = new MqClientAdminImpl(remotingClient); + when(remotingClient.invoke(any(String.class), any(RemotingCommand.class), any(Long.class))).thenReturn(CompletableFuture.completedFuture(response)); + } + + @Test + public void assertQueryMessageWithSuccess() throws Exception { + setResponseSuccess(getMessageResult()); + QueryMessageRequestHeader requestHeader = mock(QueryMessageRequestHeader.class); + when(requestHeader.getTopic()).thenReturn(defaultTopic); + when(requestHeader.getKey()).thenReturn("keys"); + CompletableFuture> actual = mqClientAdminImpl.queryMessage(defaultBrokerAddr, false, false, requestHeader, defaultTimeout); + List messageExtList = actual.get(); + assertNotNull(messageExtList); + assertEquals(1, messageExtList.size()); + } + + @Test + public void assertQueryMessageWithNotFound() throws Exception { + when(response.getCode()).thenReturn(ResponseCode.QUERY_NOT_FOUND); + QueryMessageRequestHeader requestHeader = mock(QueryMessageRequestHeader.class); + CompletableFuture> actual = mqClientAdminImpl.queryMessage(defaultBrokerAddr, false, false, requestHeader, defaultTimeout); + List messageExtList = actual.get(); + assertNotNull(messageExtList); + assertEquals(0, messageExtList.size()); + } + + @Test + public void assertQueryMessageWithError() { + setResponseError(); + QueryMessageRequestHeader requestHeader = mock(QueryMessageRequestHeader.class); + CompletableFuture> actual = mqClientAdminImpl.queryMessage(defaultBrokerAddr, false, false, requestHeader, defaultTimeout); + Throwable thrown = assertThrows(ExecutionException.class, actual::get); + assertTrue(thrown.getCause() instanceof MQClientException); + MQClientException mqException = (MQClientException) thrown.getCause(); + assertEquals(ResponseCode.SYSTEM_ERROR, mqException.getResponseCode()); + assertTrue(mqException.getMessage().contains("CODE: 1 DESC: null")); + } + + @Test + public void assertGetTopicStatsInfoWithSuccess() throws Exception { + TopicStatsTable responseBody = new TopicStatsTable(); + setResponseSuccess(RemotingSerializable.encode(responseBody)); + GetTopicStatsInfoRequestHeader requestHeader = mock(GetTopicStatsInfoRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.getTopicStatsInfo(defaultBrokerAddr, requestHeader, defaultTimeout); + TopicStatsTable topicStatsTable = actual.get(); + assertNotNull(topicStatsTable); + assertEquals(0, topicStatsTable.getOffsetTable().size()); + } + + @Test + public void assertGetTopicStatsInfoWithError() { + setResponseError(); + GetTopicStatsInfoRequestHeader requestHeader = mock(GetTopicStatsInfoRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.getTopicStatsInfo(defaultBrokerAddr, requestHeader, defaultTimeout); + Throwable thrown = assertThrows(ExecutionException.class, actual::get); + assertTrue(thrown.getCause() instanceof MQClientException); + MQClientException mqException = (MQClientException) thrown.getCause(); + assertEquals(ResponseCode.SYSTEM_ERROR, mqException.getResponseCode()); + assertTrue(mqException.getMessage().contains("CODE: 1 DESC: null")); + } + + @Test + public void assertQueryConsumeTimeSpanWithSuccess() throws Exception { + QueryConsumeTimeSpanBody responseBody = new QueryConsumeTimeSpanBody(); + setResponseSuccess(RemotingSerializable.encode(responseBody)); + QueryConsumeTimeSpanRequestHeader requestHeader = mock(QueryConsumeTimeSpanRequestHeader.class); + CompletableFuture> actual = mqClientAdminImpl.queryConsumeTimeSpan(defaultBrokerAddr, requestHeader, defaultTimeout); + List queueTimeSpans = actual.get(); + assertNotNull(queueTimeSpans); + assertEquals(0, queueTimeSpans.size()); + } + + @Test + public void assertQueryConsumeTimeSpanWithError() { + setResponseError(); + QueryConsumeTimeSpanRequestHeader requestHeader = mock(QueryConsumeTimeSpanRequestHeader.class); + CompletableFuture> actual = mqClientAdminImpl.queryConsumeTimeSpan(defaultBrokerAddr, requestHeader, defaultTimeout); + Throwable thrown = assertThrows(ExecutionException.class, actual::get); + assertTrue(thrown.getCause() instanceof MQClientException); + MQClientException mqException = (MQClientException) thrown.getCause(); + assertEquals(ResponseCode.SYSTEM_ERROR, mqException.getResponseCode()); + assertTrue(mqException.getMessage().contains("CODE: 1 DESC: null")); + } + + @Test + public void assertUpdateOrCreateTopicWithSuccess() throws Exception { + setResponseSuccess(null); + CreateTopicRequestHeader requestHeader = mock(CreateTopicRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.updateOrCreateTopic(defaultBrokerAddr, requestHeader, defaultTimeout); + assertNull(actual.get()); + } + + @Test + public void assertUpdateOrCreateTopicWithError() { + setResponseError(); + CreateTopicRequestHeader requestHeader = mock(CreateTopicRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.updateOrCreateTopic(defaultBrokerAddr, requestHeader, defaultTimeout); + Throwable thrown = assertThrows(ExecutionException.class, actual::get); + assertTrue(thrown.getCause() instanceof MQClientException); + MQClientException mqException = (MQClientException) thrown.getCause(); + assertEquals(ResponseCode.SYSTEM_ERROR, mqException.getResponseCode()); + assertTrue(mqException.getMessage().contains("CODE: 1 DESC: null")); + } + + @Test + public void assertUpdateOrCreateSubscriptionGroupWithSuccess() throws Exception { + setResponseSuccess(null); + SubscriptionGroupConfig config = mock(SubscriptionGroupConfig.class); + CompletableFuture actual = mqClientAdminImpl.updateOrCreateSubscriptionGroup(defaultBrokerAddr, config, defaultTimeout); + assertNull(actual.get()); + } + + @Test + public void assertUpdateOrCreateSubscriptionGroupWithError() { + setResponseError(); + SubscriptionGroupConfig config = mock(SubscriptionGroupConfig.class); + CompletableFuture actual = mqClientAdminImpl.updateOrCreateSubscriptionGroup(defaultBrokerAddr, config, defaultTimeout); + Throwable thrown = assertThrows(ExecutionException.class, actual::get); + assertTrue(thrown.getCause() instanceof MQClientException); + MQClientException mqException = (MQClientException) thrown.getCause(); + assertEquals(ResponseCode.SYSTEM_ERROR, mqException.getResponseCode()); + assertTrue(mqException.getMessage().contains("CODE: 1 DESC: null")); + } + + @Test + public void assertDeleteTopicInBrokerWithSuccess() throws Exception { + setResponseSuccess(null); + DeleteTopicRequestHeader requestHeader = mock(DeleteTopicRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.deleteTopicInBroker(defaultBrokerAddr, requestHeader, defaultTimeout); + assertNull(actual.get()); + } + + @Test + public void assertDeleteTopicInBrokerWithError() { + setResponseError(); + DeleteTopicRequestHeader requestHeader = mock(DeleteTopicRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.deleteTopicInBroker(defaultBrokerAddr, requestHeader, defaultTimeout); + Throwable thrown = assertThrows(ExecutionException.class, actual::get); + assertTrue(thrown.getCause() instanceof MQClientException); + MQClientException mqException = (MQClientException) thrown.getCause(); + assertEquals(ResponseCode.SYSTEM_ERROR, mqException.getResponseCode()); + assertTrue(mqException.getMessage().contains("CODE: 1 DESC: null")); + } + + @Test + public void assertDeleteTopicInNameserverWithSuccess() throws Exception { + setResponseSuccess(null); + DeleteTopicFromNamesrvRequestHeader requestHeader = mock(DeleteTopicFromNamesrvRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.deleteTopicInNameserver(defaultBrokerAddr, requestHeader, defaultTimeout); + assertNull(actual.get()); + } + + @Test + public void assertDeleteTopicInNameserverWithError() { + setResponseError(); + DeleteTopicFromNamesrvRequestHeader requestHeader = mock(DeleteTopicFromNamesrvRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.deleteTopicInNameserver(defaultBrokerAddr, requestHeader, defaultTimeout); + Throwable thrown = assertThrows(ExecutionException.class, actual::get); + assertTrue(thrown.getCause() instanceof MQClientException); + MQClientException mqException = (MQClientException) thrown.getCause(); + assertEquals(ResponseCode.SYSTEM_ERROR, mqException.getResponseCode()); + assertTrue(mqException.getMessage().contains("CODE: 1 DESC: null")); + } + + @Test + public void assertDeleteKvConfigWithSuccess() throws Exception { + setResponseSuccess(null); + DeleteKVConfigRequestHeader requestHeader = mock(DeleteKVConfigRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.deleteKvConfig(defaultBrokerAddr, requestHeader, defaultTimeout); + assertNull(actual.get()); + } + + @Test + public void assertDeleteKvConfigWithError() { + setResponseError(); + DeleteKVConfigRequestHeader requestHeader = mock(DeleteKVConfigRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.deleteKvConfig(defaultBrokerAddr, requestHeader, defaultTimeout); + Throwable thrown = assertThrows(ExecutionException.class, actual::get); + assertTrue(thrown.getCause() instanceof MQClientException); + MQClientException mqException = (MQClientException) thrown.getCause(); + assertEquals(ResponseCode.SYSTEM_ERROR, mqException.getResponseCode()); + assertTrue(mqException.getMessage().contains("CODE: 1 DESC: null")); + } + + @Test + public void assertDeleteSubscriptionGroupWithSuccess() throws Exception { + setResponseSuccess(null); + DeleteSubscriptionGroupRequestHeader requestHeader = mock(DeleteSubscriptionGroupRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.deleteSubscriptionGroup(defaultBrokerAddr, requestHeader, defaultTimeout); + assertNull(actual.get()); + } + + @Test + public void assertDeleteSubscriptionGroupWithError() { + setResponseError(); + DeleteSubscriptionGroupRequestHeader requestHeader = mock(DeleteSubscriptionGroupRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.deleteSubscriptionGroup(defaultBrokerAddr, requestHeader, defaultTimeout); + Throwable thrown = assertThrows(ExecutionException.class, actual::get); + assertTrue(thrown.getCause() instanceof MQClientException); + MQClientException mqException = (MQClientException) thrown.getCause(); + assertEquals(ResponseCode.SYSTEM_ERROR, mqException.getResponseCode()); + assertTrue(mqException.getMessage().contains("CODE: 1 DESC: null")); + } + + @Test + public void assertInvokeBrokerToResetOffsetWithSuccess() throws Exception { + ResetOffsetBody responseBody = new ResetOffsetBody(); + setResponseSuccess(RemotingSerializable.encode(responseBody)); + ResetOffsetRequestHeader requestHeader = mock(ResetOffsetRequestHeader.class); + CompletableFuture> actual = mqClientAdminImpl.invokeBrokerToResetOffset(defaultBrokerAddr, requestHeader, defaultTimeout); + assertEquals(0, actual.get().size()); + } + + @Test + public void assertInvokeBrokerToResetOffsetWithError() { + setResponseError(); + ResetOffsetRequestHeader requestHeader = mock(ResetOffsetRequestHeader.class); + CompletableFuture> actual = mqClientAdminImpl.invokeBrokerToResetOffset(defaultBrokerAddr, requestHeader, defaultTimeout); + Throwable thrown = assertThrows(ExecutionException.class, actual::get); + assertTrue(thrown.getCause() instanceof MQClientException); + MQClientException mqException = (MQClientException) thrown.getCause(); + assertEquals(ResponseCode.SYSTEM_ERROR, mqException.getResponseCode()); + assertTrue(mqException.getMessage().contains("CODE: 1 DESC: null")); + } + + @Test + public void assertViewMessageWithSuccess() throws Exception { + setResponseSuccess(getMessageResult()); + ViewMessageRequestHeader requestHeader = mock(ViewMessageRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.viewMessage(defaultBrokerAddr, requestHeader, defaultTimeout); + MessageExt result = actual.get(); + assertNotNull(result); + assertEquals(defaultTopic, result.getTopic()); + } + + @Test + public void assertViewMessageWithError() { + setResponseError(); + ViewMessageRequestHeader requestHeader = mock(ViewMessageRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.viewMessage(defaultBrokerAddr, requestHeader, defaultTimeout); + Throwable thrown = assertThrows(ExecutionException.class, actual::get); + assertTrue(thrown.getCause() instanceof MQClientException); + MQClientException mqException = (MQClientException) thrown.getCause(); + assertEquals(ResponseCode.SYSTEM_ERROR, mqException.getResponseCode()); + assertTrue(mqException.getMessage().contains("CODE: 1 DESC: null")); + } + + @Test + public void assertGetBrokerClusterInfoWithSuccess() throws Exception { + ClusterInfo responseBody = new ClusterInfo(); + setResponseSuccess(RemotingSerializable.encode(responseBody)); + CompletableFuture actual = mqClientAdminImpl.getBrokerClusterInfo(defaultBrokerAddr, defaultTimeout); + ClusterInfo result = actual.get(); + assertNotNull(result); + } + + @Test + public void assertGetBrokerClusterInfoWithError() { + setResponseError(); + CompletableFuture actual = mqClientAdminImpl.getBrokerClusterInfo(defaultBrokerAddr, defaultTimeout); + Throwable thrown = assertThrows(ExecutionException.class, actual::get); + assertTrue(thrown.getCause() instanceof MQClientException); + MQClientException mqException = (MQClientException) thrown.getCause(); + assertEquals(ResponseCode.SYSTEM_ERROR, mqException.getResponseCode()); + assertTrue(mqException.getMessage().contains("CODE: 1 DESC: null")); + } + + @Test + public void assertGetConsumerConnectionListWithSuccess() throws Exception { + ConsumerConnection responseBody = new ConsumerConnection(); + setResponseSuccess(RemotingSerializable.encode(responseBody)); + GetConsumerConnectionListRequestHeader requestHeader = mock(GetConsumerConnectionListRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.getConsumerConnectionList(defaultBrokerAddr, requestHeader, defaultTimeout); + ConsumerConnection result = actual.get(); + assertNotNull(result); + assertEquals(0, result.getConnectionSet().size()); + } + + @Test + public void assertGetConsumerConnectionListWithError() { + setResponseError(); + GetConsumerConnectionListRequestHeader requestHeader = mock(GetConsumerConnectionListRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.getConsumerConnectionList(defaultBrokerAddr, requestHeader, defaultTimeout); + Throwable thrown = assertThrows(ExecutionException.class, actual::get); + assertTrue(thrown.getCause() instanceof MQClientException); + MQClientException mqException = (MQClientException) thrown.getCause(); + assertEquals(ResponseCode.SYSTEM_ERROR, mqException.getResponseCode()); + assertTrue(mqException.getMessage().contains("CODE: 1 DESC: null")); + } + + @Test + public void assertQueryTopicsByConsumerWithSuccess() throws Exception { + TopicList responseBody = new TopicList(); + setResponseSuccess(RemotingSerializable.encode(responseBody)); + QueryTopicsByConsumerRequestHeader requestHeader = mock(QueryTopicsByConsumerRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.queryTopicsByConsumer(defaultBrokerAddr, requestHeader, defaultTimeout); + TopicList result = actual.get(); + assertNotNull(result); + assertEquals(0, result.getTopicList().size()); + } + + @Test + public void assertQueryTopicsByConsumerWithError() { + setResponseError(); + QueryTopicsByConsumerRequestHeader requestHeader = mock(QueryTopicsByConsumerRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.queryTopicsByConsumer(defaultBrokerAddr, requestHeader, defaultTimeout); + Throwable thrown = assertThrows(ExecutionException.class, actual::get); + assertTrue(thrown.getCause() instanceof MQClientException); + MQClientException mqException = (MQClientException) thrown.getCause(); + assertEquals(ResponseCode.SYSTEM_ERROR, mqException.getResponseCode()); + assertTrue(mqException.getMessage().contains("CODE: 1 DESC: null")); + } + + @Test + public void assertQuerySubscriptionByConsumerWithSuccess() throws Exception { + SubscriptionData responseBody = new SubscriptionData(); + setResponseSuccess(RemotingSerializable.encode(responseBody)); + QuerySubscriptionByConsumerRequestHeader requestHeader = mock(QuerySubscriptionByConsumerRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.querySubscriptionByConsumer(defaultBrokerAddr, requestHeader, defaultTimeout); + assertNull(actual.get()); + } + + @Test + public void assertQuerySubscriptionByConsumerWithError() { + setResponseError(); + QuerySubscriptionByConsumerRequestHeader requestHeader = mock(QuerySubscriptionByConsumerRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.querySubscriptionByConsumer(defaultBrokerAddr, requestHeader, defaultTimeout); + Throwable thrown = assertThrows(ExecutionException.class, actual::get); + assertTrue(thrown.getCause() instanceof MQClientException); + MQClientException mqException = (MQClientException) thrown.getCause(); + assertEquals(ResponseCode.SYSTEM_ERROR, mqException.getResponseCode()); + assertTrue(mqException.getMessage().contains("CODE: 1 DESC: null")); + } + + @Test + public void assertGetConsumeStatsWithSuccess() throws Exception { + ConsumeStats responseBody = new ConsumeStats(); + setResponseSuccess(RemotingSerializable.encode(responseBody)); + GetConsumeStatsRequestHeader requestHeader = mock(GetConsumeStatsRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.getConsumeStats(defaultBrokerAddr, requestHeader, defaultTimeout); + ConsumeStats result = actual.get(); + assertNotNull(result); + assertEquals(0, result.getOffsetTable().size()); + } + + @Test + public void assertGetConsumeStatsWithError() { + setResponseError(); + GetConsumeStatsRequestHeader requestHeader = mock(GetConsumeStatsRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.getConsumeStats(defaultBrokerAddr, requestHeader, defaultTimeout); + Throwable thrown = assertThrows(ExecutionException.class, actual::get); + assertTrue(thrown.getCause() instanceof MQClientException); + MQClientException mqException = (MQClientException) thrown.getCause(); + assertEquals(ResponseCode.SYSTEM_ERROR, mqException.getResponseCode()); + assertTrue(mqException.getMessage().contains("CODE: 1 DESC: null")); + } + + @Test + public void assertQueryTopicConsumeByWhoWithSuccess() throws Exception { + GroupList responseBody = new GroupList(); + setResponseSuccess(RemotingSerializable.encode(responseBody)); + QueryTopicConsumeByWhoRequestHeader requestHeader = mock(QueryTopicConsumeByWhoRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.queryTopicConsumeByWho(defaultBrokerAddr, requestHeader, defaultTimeout); + GroupList result = actual.get(); + assertNotNull(result); + assertEquals(0, result.getGroupList().size()); + } + + @Test + public void assertQueryTopicConsumeByWhoWithError() { + setResponseError(); + QueryTopicConsumeByWhoRequestHeader requestHeader = mock(QueryTopicConsumeByWhoRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.queryTopicConsumeByWho(defaultBrokerAddr, requestHeader, defaultTimeout); + Throwable thrown = assertThrows(ExecutionException.class, actual::get); + assertTrue(thrown.getCause() instanceof MQClientException); + MQClientException mqException = (MQClientException) thrown.getCause(); + assertEquals(ResponseCode.SYSTEM_ERROR, mqException.getResponseCode()); + assertTrue(mqException.getMessage().contains("CODE: 1 DESC: null")); + } + + @Test + public void assertGetConsumerRunningInfoWithSuccess() throws Exception { + ConsumerRunningInfo responseBody = new ConsumerRunningInfo(); + setResponseSuccess(RemotingSerializable.encode(responseBody)); + GetConsumerRunningInfoRequestHeader requestHeader = mock(GetConsumerRunningInfoRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.getConsumerRunningInfo(defaultBrokerAddr, requestHeader, defaultTimeout); + ConsumerRunningInfo result = actual.get(); + assertNotNull(result); + assertEquals(0, result.getProperties().size()); + } + + @Test + public void assertGetConsumerRunningInfoWithError() { + setResponseError(); + GetConsumerRunningInfoRequestHeader requestHeader = mock(GetConsumerRunningInfoRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.getConsumerRunningInfo(defaultBrokerAddr, requestHeader, defaultTimeout); + Throwable thrown = assertThrows(ExecutionException.class, actual::get); + assertTrue(thrown.getCause() instanceof MQClientException); + MQClientException mqException = (MQClientException) thrown.getCause(); + assertEquals(ResponseCode.SYSTEM_ERROR, mqException.getResponseCode()); + assertTrue(mqException.getMessage().contains("CODE: 1 DESC: null")); + } + + @Test + public void assertConsumeMessageDirectlyWithSuccess() throws Exception { + ConsumeMessageDirectlyResult responseBody = new ConsumeMessageDirectlyResult(); + setResponseSuccess(RemotingSerializable.encode(responseBody)); + ConsumeMessageDirectlyResultRequestHeader requestHeader = mock(ConsumeMessageDirectlyResultRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.consumeMessageDirectly(defaultBrokerAddr, requestHeader, defaultTimeout); + ConsumeMessageDirectlyResult result = actual.get(); + assertNotNull(result); + assertTrue(result.isAutoCommit()); + } + + @Test + public void assertConsumeMessageDirectlyWithError() { + setResponseError(); + ConsumeMessageDirectlyResultRequestHeader requestHeader = mock(ConsumeMessageDirectlyResultRequestHeader.class); + CompletableFuture actual = mqClientAdminImpl.consumeMessageDirectly(defaultBrokerAddr, requestHeader, defaultTimeout); + Throwable thrown = assertThrows(ExecutionException.class, actual::get); + assertTrue(thrown.getCause() instanceof MQClientException); + MQClientException mqException = (MQClientException) thrown.getCause(); + assertEquals(ResponseCode.SYSTEM_ERROR, mqException.getResponseCode()); + assertTrue(mqException.getMessage().contains("CODE: 1 DESC: null")); + } + + private byte[] getMessageResult() throws Exception { + byte[] bytes = MessageDecoder.encode(createMessageExt(), false); + ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length); + byteBuffer.put(bytes); + return byteBuffer.array(); + } + + private MessageExt createMessageExt() { + MessageExt result = new MessageExt(); + result.setBody("body".getBytes(StandardCharsets.UTF_8)); + result.setTopic(defaultTopic); + result.setBrokerName("defaultBroker"); + result.putUserProperty("key", "value"); + result.getProperties().put(MessageConst.PROPERTY_PRODUCER_GROUP, "defaultGroup"); + result.getProperties().put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, "TX1"); + result.setKeys("keys"); + SocketAddress bornHost = new InetSocketAddress("127.0.0.1", 12911); + SocketAddress storeHost = new InetSocketAddress("127.0.0.1", 10911); + result.setBornHost(bornHost); + result.setStoreHost(storeHost); + return result; + } + + private void setResponseSuccess(byte[] body) { + when(response.getCode()).thenReturn(ResponseCode.SUCCESS); + when(response.getBody()).thenReturn(body); + } + + private void setResponseError() { + when(response.getCode()).thenReturn(ResponseCode.SYSTEM_ERROR); + } +} 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 0000000..395c0ff --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageConcurrentlyServiceTest.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.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.AfterClass; +import org.junit.BeforeClass; +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; +import org.mockito.Mockito; + +@RunWith(MockitoJUnitRunner.class) +public class ConsumeMessageConcurrentlyServiceTest { + + private static String consumerGroup; + + private static String topic = "FooBar"; + + private static String brokerName = "BrokerA"; + + private static MQClientInstance mQClientFactory; + + @Mock + private static MQClientAPIImpl mQClientAPIImpl; + + private static PullAPIWrapper pullAPIWrapper; + + private static RebalancePushImpl rebalancePushImpl; + + private static DefaultMQPushConsumer pushConsumer; + + @BeforeClass + public static void init() throws Exception { + mQClientAPIImpl = Mockito.mock(MQClientAPIImpl.class); + 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' }); + } + + @AfterClass + public static void terminate() { + pushConsumer.shutdown(); + } + + private static 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 static 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 0000000..5fa78b7 --- /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/ConsumeMessagePopConcurrentlyServiceTest.java b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopConcurrentlyServiceTest.java new file mode 100644 index 0000000..5097f14 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopConcurrentlyServiceTest.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.client.impl.consumer; + +import org.apache.commons.lang3.reflect.FieldUtils; +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.stat.ConsumerStatsManager; +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.remoting.protocol.body.CMResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +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 ConsumeMessagePopConcurrentlyServiceTest { + + @Mock + private DefaultMQPushConsumerImpl defaultMQPushConsumerImpl; + + @Mock + private MessageListenerConcurrently messageListener; + + @Mock + private DefaultMQPushConsumer defaultMQPushConsumer; + + private ConsumeMessagePopConcurrentlyService popService; + + private final String defaultGroup = "defaultGroup"; + + private final String defaultBroker = "defaultBroker"; + + private final String defaultTopic = "defaultTopic"; + + @Before + public void init() throws Exception { + when(defaultMQPushConsumer.getConsumerGroup()).thenReturn(defaultGroup); + when(defaultMQPushConsumer.getConsumeThreadMin()).thenReturn(1); + when(defaultMQPushConsumer.getConsumeThreadMax()).thenReturn(3); + when(defaultMQPushConsumer.getConsumeMessageBatchMaxSize()).thenReturn(32); + when(defaultMQPushConsumerImpl.getDefaultMQPushConsumer()).thenReturn(defaultMQPushConsumer); + ConsumerStatsManager consumerStatsManager = mock(ConsumerStatsManager.class); + when(defaultMQPushConsumerImpl.getConsumerStatsManager()).thenReturn(consumerStatsManager); + popService = new ConsumeMessagePopConcurrentlyService(defaultMQPushConsumerImpl, messageListener); + } + + @Test + public void testUpdateCorePoolSize() { + popService.updateCorePoolSize(2); + popService.incCorePoolSize(); + popService.decCorePoolSize(); + assertEquals(2, popService.getCorePoolSize()); + } + + @Test + public void testConsumeMessageDirectly() { + when(messageListener.consumeMessage(any(), any(ConsumeConcurrentlyContext.class))).thenReturn(ConsumeConcurrentlyStatus.CONSUME_SUCCESS); + ConsumeMessageDirectlyResult actual = popService.consumeMessageDirectly(createMessageExt(), defaultBroker); + assertEquals(CMResult.CR_SUCCESS, actual.getConsumeResult()); + } + + @Test + public void testConsumeMessageDirectlyWithCrLater() { + when(messageListener.consumeMessage(any(), any(ConsumeConcurrentlyContext.class))).thenReturn(ConsumeConcurrentlyStatus.RECONSUME_LATER); + ConsumeMessageDirectlyResult actual = popService.consumeMessageDirectly(createMessageExt(), defaultBroker); + assertEquals(CMResult.CR_LATER, actual.getConsumeResult()); + } + + @Test + public void testConsumeMessageDirectlyWithCrReturnNull() { + ConsumeMessageDirectlyResult actual = popService.consumeMessageDirectly(createMessageExt(), defaultBroker); + assertEquals(CMResult.CR_RETURN_NULL, actual.getConsumeResult()); + } + + @Test + public void testConsumeMessageDirectlyWithCrThrowException() { + when(messageListener.consumeMessage(any(), any(ConsumeConcurrentlyContext.class))).thenThrow(new RuntimeException("exception")); + ConsumeMessageDirectlyResult actual = popService.consumeMessageDirectly(createMessageExt(), defaultBroker); + assertEquals(CMResult.CR_THROW_EXCEPTION, actual.getConsumeResult()); + } + + @Test + public void testShutdown() throws IllegalAccessException { + popService.shutdown(3000L); + Field scheduledExecutorServiceField = FieldUtils.getDeclaredField(popService.getClass(), "scheduledExecutorService", true); + Field consumeExecutorField = FieldUtils.getDeclaredField(popService.getClass(), "consumeExecutor", true); + ScheduledExecutorService scheduledExecutorService = (ScheduledExecutorService) scheduledExecutorServiceField.get(popService); + ThreadPoolExecutor consumeExecutor = (ThreadPoolExecutor) consumeExecutorField.get(popService); + assertTrue(scheduledExecutorService.isShutdown()); + assertTrue(scheduledExecutorService.isTerminated()); + assertTrue(consumeExecutor.isShutdown()); + assertTrue(consumeExecutor.isTerminated()); + } + + @Test + public void testSubmitConsumeRequest() { + assertThrows(UnsupportedOperationException.class, () -> { + List msgs = mock(List.class); + ProcessQueue processQueue = mock(ProcessQueue.class); + MessageQueue messageQueue = mock(MessageQueue.class); + popService.submitConsumeRequest(msgs, processQueue, messageQueue, false); + }); + } + + @Test + public void testSubmitPopConsumeRequest() throws IllegalAccessException { + List msgs = Collections.singletonList(createMessageExt()); + PopProcessQueue processQueue = mock(PopProcessQueue.class); + MessageQueue messageQueue = mock(MessageQueue.class); + ThreadPoolExecutor consumeExecutor = mock(ThreadPoolExecutor.class); + FieldUtils.writeDeclaredField(popService, "consumeExecutor", consumeExecutor, true); + popService.submitPopConsumeRequest(msgs, processQueue, messageQueue); + verify(consumeExecutor, times(1)).submit(any(Runnable.class)); + } + + @Test + public void testSubmitPopConsumeRequestWithMultiMsg() throws IllegalAccessException { + List msgs = Arrays.asList(createMessageExt(), createMessageExt()); + PopProcessQueue processQueue = mock(PopProcessQueue.class); + MessageQueue messageQueue = mock(MessageQueue.class); + ThreadPoolExecutor consumeExecutor = mock(ThreadPoolExecutor.class); + FieldUtils.writeDeclaredField(popService, "consumeExecutor", consumeExecutor, true); + when(defaultMQPushConsumer.getConsumeMessageBatchMaxSize()).thenReturn(1); + popService.submitPopConsumeRequest(msgs, processQueue, messageQueue); + verify(consumeExecutor, times(2)).submit(any(Runnable.class)); + } + + @Test + public void testProcessConsumeResult() { + ConsumeConcurrentlyContext context = mock(ConsumeConcurrentlyContext.class); + ConsumeMessagePopConcurrentlyService.ConsumeRequest consumeRequest = mock(ConsumeMessagePopConcurrentlyService.ConsumeRequest.class); + when(consumeRequest.getMsgs()).thenReturn(Arrays.asList(createMessageExt(), createMessageExt())); + MessageQueue messageQueue = mock(MessageQueue.class); + when(messageQueue.getTopic()).thenReturn(defaultTopic); + when(consumeRequest.getMessageQueue()).thenReturn(messageQueue); + PopProcessQueue processQueue = mock(PopProcessQueue.class); + when(processQueue.ack()).thenReturn(0); + when(consumeRequest.getPopProcessQueue()).thenReturn(processQueue); + when(defaultMQPushConsumerImpl.getPopDelayLevel()).thenReturn(new int[]{1, 10}); + popService.processConsumeResult(ConsumeConcurrentlyStatus.CONSUME_SUCCESS, context, consumeRequest); + verify(defaultMQPushConsumerImpl, times(1)).ackAsync(any(MessageExt.class), any()); + } + + private MessageExt createMessageExt() { + MessageExt result = new MessageExt(); + result.setBody("body".getBytes(StandardCharsets.UTF_8)); + result.setTopic(defaultTopic); + result.setBrokerName(defaultBroker); + result.putUserProperty("key", "value"); + result.getProperties().put(MessageConst.PROPERTY_PRODUCER_GROUP, defaultGroup); + result.getProperties().put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, "TX1"); + long curTime = System.currentTimeMillis(); + result.setBornTimestamp(curTime - 1000); + result.getProperties().put(MessageConst.PROPERTY_POP_CK, curTime + " " + curTime + " " + curTime + " " + curTime); + result.setKeys("keys"); + SocketAddress bornHost = new InetSocketAddress("127.0.0.1", 12911); + SocketAddress storeHost = new InetSocketAddress("127.0.0.1", 10911); + result.setBornHost(bornHost); + result.setStoreHost(storeHost); + return result; + } +} diff --git a/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopOrderlyServiceTest.java b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopOrderlyServiceTest.java new file mode 100644 index 0000000..257783e --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopOrderlyServiceTest.java @@ -0,0 +1,243 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.commons.lang3.reflect.FieldUtils; +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.impl.factory.MQClientInstance; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.stat.ConsumerStatsManager; +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.remoting.protocol.body.CMResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +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 ConsumeMessagePopOrderlyServiceTest { + + @Mock + private DefaultMQPushConsumerImpl defaultMQPushConsumerImpl; + + @Mock + private MessageListenerOrderly messageListener; + + @Mock + private DefaultMQPushConsumer defaultMQPushConsumer; + + @Mock + private ConsumerStatsManager consumerStatsManager; + + @Mock + private RebalanceImpl rebalanceImpl; + + private ConsumeMessagePopOrderlyService popService; + + private final String defaultGroup = "defaultGroup"; + + private final String defaultBroker = "defaultBroker"; + + private final String defaultTopic = "defaultTopic"; + + @Before + public void init() throws Exception { + when(defaultMQPushConsumer.getConsumerGroup()).thenReturn(defaultGroup); + when(defaultMQPushConsumer.getConsumeThreadMin()).thenReturn(1); + when(defaultMQPushConsumer.getConsumeThreadMax()).thenReturn(3); + when(defaultMQPushConsumerImpl.getDefaultMQPushConsumer()).thenReturn(defaultMQPushConsumer); + when(defaultMQPushConsumerImpl.getRebalanceImpl()).thenReturn(rebalanceImpl); + when(defaultMQPushConsumerImpl.getConsumerStatsManager()).thenReturn(consumerStatsManager); + MQClientInstance mQClientFactory = mock(MQClientInstance.class); + DefaultMQProducer defaultMQProducer = mock(DefaultMQProducer.class); + when(mQClientFactory.getDefaultMQProducer()).thenReturn(defaultMQProducer); + when(defaultMQPushConsumerImpl.getmQClientFactory()).thenReturn(mQClientFactory); + popService = new ConsumeMessagePopOrderlyService(defaultMQPushConsumerImpl, messageListener); + } + + @Test + public void testShutdown() throws IllegalAccessException { + popService.shutdown(3000L); + Field scheduledExecutorServiceField = FieldUtils.getDeclaredField(popService.getClass(), "scheduledExecutorService", true); + Field consumeExecutorField = FieldUtils.getDeclaredField(popService.getClass(), "consumeExecutor", true); + ScheduledExecutorService scheduledExecutorService = (ScheduledExecutorService) scheduledExecutorServiceField.get(popService); + ThreadPoolExecutor consumeExecutor = (ThreadPoolExecutor) consumeExecutorField.get(popService); + assertTrue(scheduledExecutorService.isShutdown()); + assertTrue(scheduledExecutorService.isTerminated()); + assertTrue(consumeExecutor.isShutdown()); + assertTrue(consumeExecutor.isTerminated()); + } + + @Test + public void testUnlockAllMessageQueues() { + popService.unlockAllMessageQueues(); + verify(rebalanceImpl, times(1)).unlockAll(eq(false)); + } + + @Test + public void testUpdateCorePoolSize() { + popService.updateCorePoolSize(2); + popService.incCorePoolSize(); + popService.decCorePoolSize(); + assertEquals(2, popService.getCorePoolSize()); + } + + @Test + public void testConsumeMessageDirectly() { + when(messageListener.consumeMessage(any(), any(ConsumeOrderlyContext.class))).thenReturn(ConsumeOrderlyStatus.SUCCESS); + ConsumeMessageDirectlyResult actual = popService.consumeMessageDirectly(createMessageExt(), defaultBroker); + assertEquals(CMResult.CR_SUCCESS, actual.getConsumeResult()); + assertTrue(actual.isOrder()); + } + + @Test + public void testConsumeMessageDirectlyWithCommit() { + when(messageListener.consumeMessage(any(), any(ConsumeOrderlyContext.class))).thenReturn(ConsumeOrderlyStatus.COMMIT); + ConsumeMessageDirectlyResult actual = popService.consumeMessageDirectly(createMessageExt(), defaultBroker); + assertEquals(CMResult.CR_COMMIT, actual.getConsumeResult()); + assertTrue(actual.isOrder()); + } + + @Test + public void testConsumeMessageDirectlyWithRollback() { + when(messageListener.consumeMessage(any(), any(ConsumeOrderlyContext.class))).thenReturn(ConsumeOrderlyStatus.ROLLBACK); + ConsumeMessageDirectlyResult actual = popService.consumeMessageDirectly(createMessageExt(), defaultBroker); + assertEquals(CMResult.CR_ROLLBACK, actual.getConsumeResult()); + assertTrue(actual.isOrder()); + } + + @Test + public void testConsumeMessageDirectlyWithCrLater() { + when(messageListener.consumeMessage(any(), any(ConsumeOrderlyContext.class))).thenReturn(ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT); + ConsumeMessageDirectlyResult actual = popService.consumeMessageDirectly(createMessageExt(), defaultBroker); + assertEquals(CMResult.CR_LATER, actual.getConsumeResult()); + } + + @Test + public void testConsumeMessageDirectlyWithCrReturnNull() { + ConsumeMessageDirectlyResult actual = popService.consumeMessageDirectly(createMessageExt(), defaultBroker); + assertEquals(CMResult.CR_RETURN_NULL, actual.getConsumeResult()); + } + + @Test + public void testConsumeMessageDirectlyWithCrThrowException() { + when(messageListener.consumeMessage(any(), any(ConsumeOrderlyContext.class))).thenThrow(new RuntimeException("exception")); + ConsumeMessageDirectlyResult actual = popService.consumeMessageDirectly(createMessageExt(), defaultBroker); + assertEquals(CMResult.CR_THROW_EXCEPTION, actual.getConsumeResult()); + } + + @Test + public void testSubmitConsumeRequest() { + assertThrows(UnsupportedOperationException.class, () -> { + List msgs = mock(List.class); + ProcessQueue processQueue = mock(ProcessQueue.class); + MessageQueue messageQueue = mock(MessageQueue.class); + popService.submitConsumeRequest(msgs, processQueue, messageQueue, false); + }); + } + + @Test + public void testSubmitPopConsumeRequest() throws IllegalAccessException { + List msgs = Collections.singletonList(createMessageExt()); + PopProcessQueue processQueue = mock(PopProcessQueue.class); + MessageQueue messageQueue = mock(MessageQueue.class); + ThreadPoolExecutor consumeExecutor = mock(ThreadPoolExecutor.class); + FieldUtils.writeDeclaredField(popService, "consumeExecutor", consumeExecutor, true); + popService.submitPopConsumeRequest(msgs, processQueue, messageQueue); + verify(consumeExecutor, times(1)).submit(any(Runnable.class)); + } + + @Test + public void testLockMQPeriodically() { + popService.lockMQPeriodically(); + verify(defaultMQPushConsumerImpl, times(1)).getRebalanceImpl(); + verify(rebalanceImpl, times(1)).lockAll(); + } + + @Test + public void testGetConsumerStatsManager() { + ConsumerStatsManager actual = popService.getConsumerStatsManager(); + assertNotNull(actual); + assertEquals(consumerStatsManager, actual); + } + + @Test + public void testSendMessageBack() { + assertTrue(popService.sendMessageBack(createMessageExt())); + } + + @Test + public void testProcessConsumeResult() { + ConsumeOrderlyContext context = mock(ConsumeOrderlyContext.class); + ConsumeMessagePopOrderlyService.ConsumeRequest consumeRequest = mock(ConsumeMessagePopOrderlyService.ConsumeRequest.class); + assertTrue(popService.processConsumeResult(Collections.singletonList(createMessageExt()), ConsumeOrderlyStatus.SUCCESS, context, consumeRequest)); + } + + @Test + public void testResetNamespace() { + when(defaultMQPushConsumer.getNamespace()).thenReturn("defaultNamespace"); + List msgs = Collections.singletonList(createMessageExt()); + popService.resetNamespace(msgs); + assertEquals(defaultTopic, msgs.get(0).getTopic()); + } + + private MessageExt createMessageExt() { + MessageExt result = new MessageExt(); + result.setBody("body".getBytes(StandardCharsets.UTF_8)); + result.setTopic(defaultTopic); + result.setBrokerName(defaultBroker); + result.putUserProperty("key", "value"); + result.getProperties().put(MessageConst.PROPERTY_PRODUCER_GROUP, defaultGroup); + result.getProperties().put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, "TX1"); + long curTime = System.currentTimeMillis(); + result.setBornTimestamp(curTime - 1000); + result.getProperties().put(MessageConst.PROPERTY_POP_CK, curTime + " " + curTime + " " + curTime + " " + curTime); + result.setKeys("keys"); + SocketAddress bornHost = new InetSocketAddress("127.0.0.1", 12911); + SocketAddress storeHost = new InetSocketAddress("127.0.0.1", 10911); + result.setBornHost(bornHost); + result.setStoreHost(storeHost); + return result; + } +} diff --git a/client/src/test/java/org/apache/rocketmq/client/impl/consumer/DefaultLitePullConsumerImplTest.java b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/DefaultLitePullConsumerImplTest.java new file mode 100644 index 0000000..3986c49 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/DefaultLitePullConsumerImplTest.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.impl.consumer; + +import org.apache.rocketmq.client.consumer.DefaultLitePullConsumer; +import org.apache.rocketmq.common.message.MessageQueue; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Set; + + +public class DefaultLitePullConsumerImplTest { + private final DefaultLitePullConsumerImpl consumer = new DefaultLitePullConsumerImpl(new DefaultLitePullConsumer(), null); + + private static Method isSetEqualMethod; + + @BeforeClass + public static void initReflectionMethod() throws NoSuchMethodException { + Class consumerClass = DefaultLitePullConsumerImpl.class; + Method testMethod = consumerClass.getDeclaredMethod("isSetEqual", Set.class, Set.class); + testMethod.setAccessible(true); + isSetEqualMethod = testMethod; + } + + + /** + * The two empty sets should be equal + */ + @Test + public void testIsSetEqual1() throws InvocationTargetException, IllegalAccessException { + Set set1 = new HashSet<>(); + Set set2 = new HashSet<>(); + boolean equalResult = (boolean) isSetEqualMethod.invoke(consumer, set1, set2); + Assert.assertTrue(equalResult); + } + + + /** + * When a set has elements and one does not, the two sets are not equal + */ + @Test + public void testIsSetEqual2() throws InvocationTargetException, IllegalAccessException { + Set set1 = new HashSet<>(); + set1.add(new MessageQueue("testTopic","testBroker",111)); + Set set2 = new HashSet<>(); + boolean equalResult = (boolean) isSetEqualMethod.invoke(consumer, set1, set2); + Assert.assertFalse(equalResult); + } + + /** + * The two null sets should be equal + */ + @Test + public void testIsSetEqual3() throws InvocationTargetException, IllegalAccessException { + Set set1 = null; + Set set2 = null; + boolean equalResult = (boolean) isSetEqualMethod.invoke(consumer, set1, set2); + Assert.assertTrue(equalResult); + } + + @Test + public void testIsSetEqual4() throws InvocationTargetException, IllegalAccessException { + Set set1 = null; + Set set2 = new HashSet<>(); + boolean equalResult = (boolean) isSetEqualMethod.invoke(consumer, set1, set2); + Assert.assertFalse(equalResult); + } + + @Test + public void testIsSetEqual5() throws InvocationTargetException, IllegalAccessException { + Set set1 = new HashSet<>(); + set1.add(new MessageQueue("testTopic","testBroker",111)); + Set set2 = new HashSet<>(); + set2.add(new MessageQueue("testTopic","testBroker",111)); + boolean equalResult = (boolean) isSetEqualMethod.invoke(consumer, set1, set2); + Assert.assertTrue(equalResult); + } + +} 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 new file mode 100644 index 0000000..2bc9c5a --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/DefaultMQPushConsumerImplTest.java @@ -0,0 +1,832 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.commons.lang3.reflect.FieldUtils; +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.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.PullStatus; +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.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.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.MQAdminImpl; +import org.apache.rocketmq.client.impl.MQClientAPIImpl; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.client.stat.ConsumerStatsManager; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.ServiceState; +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.remoting.RPCHook; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.body.ConsumeStatus; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.body.QueueTimeSpan; +import org.apache.rocketmq.remoting.protocol.header.AckMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ChangeInvisibleTimeRequestHeader; +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.junit.Assert; +import org.junit.Before; +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 java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLong; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertArrayEquals; +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.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.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class DefaultMQPushConsumerImplTest { + + @Mock + private DefaultMQPushConsumer defaultMQPushConsumer; + + @Mock + private MQClientInstance mQClientFactory; + + @Mock + private RebalanceImpl rebalanceImpl; + + @Mock + private PullAPIWrapper pullAPIWrapper; + + @Mock + private PullRequest pullRequest; + + @Mock + private PopRequest popRequest; + + @Mock + private ProcessQueue processQueue; + + @Mock + private PopProcessQueue popProcessQueue; + + @Mock + private MQClientAPIImpl mqClientAPIImpl; + + @Mock + private OffsetStore offsetStore; + + private DefaultMQPushConsumerImpl defaultMQPushConsumerImpl; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private final String defaultKey = "defaultKey"; + + private final String defaultTopic = "defaultTopic"; + + private final String defaultBroker = "defaultBroker"; + + private final String defaultBrokerAddr = "127.0.0.1:10911"; + + private final String defaultGroup = "defaultGroup"; + + private final long defaultTimeout = 3000L; + + @Test + public void checkConfigTest() throws MQClientException { + + //test type + thrown.expect(MQClientException.class); + + //test message + thrown.expectMessage("consumeThreadMin (10) is larger than consumeThreadMax (9)"); + + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test_consumer_group"); + + consumer.setConsumeThreadMin(10); + consumer.setConsumeThreadMax(9); + + consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> ConsumeConcurrentlyStatus.CONSUME_SUCCESS); + + DefaultMQPushConsumerImpl defaultMQPushConsumerImpl = new DefaultMQPushConsumerImpl(consumer, null); + defaultMQPushConsumerImpl.start(); + } + + @Test + public void testHook() { + 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((MessageListenerConcurrently) (msgs, 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(); + } + } + + @Before + public void init() throws NoSuchFieldException, IllegalAccessException { + MQAdminImpl mqAdminImpl = mock(MQAdminImpl.class); + when(mQClientFactory.getMQAdminImpl()).thenReturn(mqAdminImpl); + ConsumerStatsManager consumerStatsManager = mock(ConsumerStatsManager.class); + ConsumeStatus consumeStatus = mock(ConsumeStatus.class); + when(consumerStatsManager.consumeStatus(any(), any())).thenReturn(consumeStatus); + when(mQClientFactory.getConsumerStatsManager()).thenReturn(consumerStatsManager); + when(mQClientFactory.getPullMessageService()).thenReturn(mock(PullMessageService.class)); + when(mQClientFactory.getMQClientAPIImpl()).thenReturn(mqClientAPIImpl); + FindBrokerResult findBrokerResult = mock(FindBrokerResult.class); + when(findBrokerResult.getBrokerAddr()).thenReturn(defaultBrokerAddr); + when(mQClientFactory.findBrokerAddressInSubscribe(anyString(), anyLong(), anyBoolean())).thenReturn(findBrokerResult); + Set messageQueueSet = Collections.singleton(createMessageQueue()); + ConcurrentMap> topicMessageQueueMap = new ConcurrentHashMap<>(); + topicMessageQueueMap.put(defaultTopic, messageQueueSet); + when(rebalanceImpl.getTopicSubscribeInfoTable()).thenReturn(topicMessageQueueMap); + ConcurrentMap processQueueTable = new ConcurrentHashMap<>(); + when(rebalanceImpl.getProcessQueueTable()).thenReturn(processQueueTable); + RPCHook rpcHook = mock(RPCHook.class); + defaultMQPushConsumerImpl = new DefaultMQPushConsumerImpl(defaultMQPushConsumer, rpcHook); + defaultMQPushConsumerImpl.setOffsetStore(offsetStore); + FieldUtils.writeDeclaredField(defaultMQPushConsumerImpl, "mQClientFactory", mQClientFactory, true); + FieldUtils.writeDeclaredField(defaultMQPushConsumerImpl, "rebalanceImpl", rebalanceImpl, true); + FieldUtils.writeDeclaredField(defaultMQPushConsumerImpl, "pullAPIWrapper", pullAPIWrapper, true); + FilterMessageHook filterMessageHook = mock(FilterMessageHook.class); + ArrayList filterMessageHookList = new ArrayList<>(); + filterMessageHookList.add(filterMessageHook); + ConsumeMessageService consumeMessagePopService = mock(ConsumeMessageService.class); + ConsumeMessageService consumeMessageService = mock(ConsumeMessageService.class); + FieldUtils.writeDeclaredField(defaultMQPushConsumerImpl, "filterMessageHookList", filterMessageHookList, true); + FieldUtils.writeDeclaredField(defaultMQPushConsumerImpl, "consumeMessageService", consumeMessageService, true); + FieldUtils.writeDeclaredField(defaultMQPushConsumerImpl, "consumeMessagePopService", consumeMessagePopService, true); + ConcurrentMap subscriptionDataMap = new ConcurrentHashMap<>(); + SubscriptionData subscriptionData = new SubscriptionData(); + subscriptionData.setTopic(defaultTopic); + subscriptionDataMap.put(defaultTopic, subscriptionData); + when(rebalanceImpl.getSubscriptionInner()).thenReturn(subscriptionDataMap); + } + + @Test + public void testFetchSubscribeMessageQueues() throws MQClientException { + Set actual = defaultMQPushConsumerImpl.fetchSubscribeMessageQueues(defaultTopic); + assertNotNull(actual); + Assert.assertEquals(1, actual.size()); + MessageQueue next = actual.iterator().next(); + assertEquals(defaultTopic, next.getTopic()); + assertEquals(defaultBroker, next.getBrokerName()); + assertEquals(0, next.getQueueId()); + } + + @Test + public void testEarliestMsgStoreTime() throws MQClientException { + assertEquals(0, defaultMQPushConsumerImpl.earliestMsgStoreTime(createMessageQueue())); + } + + @Test + public void testMaxOffset() throws MQClientException { + assertEquals(0, defaultMQPushConsumerImpl.maxOffset(createMessageQueue())); + } + + @Test + public void testMinOffset() throws MQClientException { + assertEquals(0, defaultMQPushConsumerImpl.minOffset(createMessageQueue())); + } + + @Test + public void testGetOffsetStore() { + assertEquals(offsetStore, defaultMQPushConsumerImpl.getOffsetStore()); + } + + @Test + public void testPullMessageWithStateNotOk() { + when(pullRequest.getProcessQueue()).thenReturn(processQueue); + defaultMQPushConsumerImpl.pullMessage(pullRequest); + } + + @Test + public void testPullMessageWithIsPause() { + when(pullRequest.getProcessQueue()).thenReturn(processQueue); + defaultMQPushConsumerImpl.setServiceState(ServiceState.RUNNING); + defaultMQPushConsumerImpl.setPause(true); + defaultMQPushConsumerImpl.pullMessage(pullRequest); + } + + @Test + public void testPullMessageWithMsgCountFlowControl() { + when(processQueue.getMsgCount()).thenReturn(new AtomicLong(2)); + when(processQueue.getMsgSize()).thenReturn(new AtomicLong(3 * 1024 * 1024)); + TreeMap treeMap = new TreeMap<>(); + treeMap.put(1L, new MessageExt()); + when(processQueue.getMsgTreeMap()).thenReturn(treeMap); + when(pullRequest.getProcessQueue()).thenReturn(processQueue); + when(defaultMQPushConsumer.getPullThresholdForQueue()).thenReturn(1); + defaultMQPushConsumerImpl.setServiceState(ServiceState.RUNNING); + defaultMQPushConsumerImpl.pullMessage(pullRequest); + } + + @Test + public void testPullMessageWithMsgSizeFlowControl() { + when(processQueue.getMsgCount()).thenReturn(new AtomicLong(2)); + when(processQueue.getMsgSize()).thenReturn(new AtomicLong(3 * 1024 * 1024)); + TreeMap treeMap = new TreeMap<>(); + treeMap.put(1L, new MessageExt()); + when(processQueue.getMsgTreeMap()).thenReturn(treeMap); + when(pullRequest.getProcessQueue()).thenReturn(processQueue); + defaultMQPushConsumerImpl.setServiceState(ServiceState.RUNNING); + when(defaultMQPushConsumer.getPullThresholdForQueue()).thenReturn(3); + when(defaultMQPushConsumer.getPullThresholdSizeForQueue()).thenReturn(1); + defaultMQPushConsumerImpl.pullMessage(pullRequest); + } + + @Test + public void testPullMessageWithMaxSpanFlowControl() { + when(processQueue.getMsgCount()).thenReturn(new AtomicLong(2)); + when(processQueue.getMaxSpan()).thenReturn(2L); + when(processQueue.getMsgSize()).thenReturn(new AtomicLong(3 * 1024 * 1024)); + TreeMap treeMap = new TreeMap<>(); + treeMap.put(1L, new MessageExt()); + when(processQueue.getMsgTreeMap()).thenReturn(treeMap); + when(pullRequest.getProcessQueue()).thenReturn(processQueue); + defaultMQPushConsumerImpl.setServiceState(ServiceState.RUNNING); + when(defaultMQPushConsumer.getPullThresholdForQueue()).thenReturn(3); + when(defaultMQPushConsumer.getPullThresholdSizeForQueue()).thenReturn(10); + defaultMQPushConsumerImpl.pullMessage(pullRequest); + } + + @Test + public void testPullMessageWithNotLocked() { + when(processQueue.getMsgCount()).thenReturn(new AtomicLong(2)); + when(processQueue.getMsgSize()).thenReturn(new AtomicLong(3 * 1024 * 1024)); + when(pullRequest.getProcessQueue()).thenReturn(processQueue); + defaultMQPushConsumerImpl.setServiceState(ServiceState.RUNNING); + defaultMQPushConsumerImpl.setConsumeOrderly(true); + when(defaultMQPushConsumer.getPullThresholdForQueue()).thenReturn(3); + when(defaultMQPushConsumer.getPullThresholdSizeForQueue()).thenReturn(10); + defaultMQPushConsumerImpl.pullMessage(pullRequest); + } + + @Test + public void testPullMessageWithSubscriptionDataIsNull() { + when(processQueue.getMsgCount()).thenReturn(new AtomicLong(2)); + when(processQueue.getMsgSize()).thenReturn(new AtomicLong(3 * 1024 * 1024)); + when(pullRequest.getMessageQueue()).thenReturn(createMessageQueue()); + when(pullRequest.getProcessQueue()).thenReturn(processQueue); + defaultMQPushConsumerImpl.setServiceState(ServiceState.RUNNING); + when(defaultMQPushConsumer.getPullThresholdForQueue()).thenReturn(3); + when(defaultMQPushConsumer.getPullThresholdSizeForQueue()).thenReturn(10); + defaultMQPushConsumerImpl.pullMessage(pullRequest); + } + + @Test + public void testPullMessageWithNoMatchedMsg() throws MQBrokerException, RemotingException, InterruptedException, MQClientException { + when(processQueue.getMsgCount()).thenReturn(new AtomicLong(2)); + when(processQueue.getMsgSize()).thenReturn(new AtomicLong(3 * 1024 * 1024)); + when(pullRequest.getMessageQueue()).thenReturn(createMessageQueue()); + when(pullRequest.getProcessQueue()).thenReturn(processQueue); + defaultMQPushConsumerImpl.setServiceState(ServiceState.RUNNING); + when(defaultMQPushConsumer.getPullThresholdForQueue()).thenReturn(3); + when(defaultMQPushConsumer.getPullThresholdSizeForQueue()).thenReturn(10); + PullResult pullResultMock = mock(PullResult.class); + when(pullAPIWrapper.processPullResult(any(MessageQueue.class), any(PullResult.class), any(SubscriptionData.class))).thenReturn(pullResultMock); + when(pullResultMock.getPullStatus()).thenReturn(PullStatus.NO_MATCHED_MSG); + doAnswer(invocation -> { + PullCallback callback = invocation.getArgument(12); + PullResult pullResult = mock(PullResult.class); + callback.onSuccess(pullResult); + return null; + }).when(pullAPIWrapper).pullKernelImpl( + any(MessageQueue.class), + any(), + any(), + anyLong(), + anyLong(), + anyInt(), + anyInt(), + anyInt(), + anyLong(), + anyLong(), + anyLong(), + any(CommunicationMode.class), + any(PullCallback.class)); + defaultMQPushConsumerImpl.pullMessage(pullRequest); + } + + @Test + public void testPullMessageWithOffsetIllegal() throws MQBrokerException, RemotingException, InterruptedException, MQClientException { + when(processQueue.getMsgCount()).thenReturn(new AtomicLong(2)); + when(processQueue.getMsgSize()).thenReturn(new AtomicLong(3 * 1024 * 1024)); + when(pullRequest.getMessageQueue()).thenReturn(createMessageQueue()); + when(pullRequest.getProcessQueue()).thenReturn(processQueue); + defaultMQPushConsumerImpl.setServiceState(ServiceState.RUNNING); + when(defaultMQPushConsumer.getPullThresholdForQueue()).thenReturn(3); + when(defaultMQPushConsumer.getPullThresholdSizeForQueue()).thenReturn(10); + PullResult pullResultMock = mock(PullResult.class); + when(pullAPIWrapper.processPullResult(any(MessageQueue.class), any(PullResult.class), any(SubscriptionData.class))).thenReturn(pullResultMock); + when(pullResultMock.getPullStatus()).thenReturn(PullStatus.OFFSET_ILLEGAL); + doAnswer(invocation -> { + PullCallback callback = invocation.getArgument(12); + PullResult pullResult = mock(PullResult.class); + callback.onSuccess(pullResult); + return null; + }).when(pullAPIWrapper).pullKernelImpl( + any(MessageQueue.class), + any(), + any(), + anyLong(), + anyLong(), + anyInt(), + anyInt(), + anyInt(), + anyLong(), + anyLong(), + anyLong(), + any(CommunicationMode.class), + any(PullCallback.class)); + defaultMQPushConsumerImpl.pullMessage(pullRequest); + } + + @Test + public void testPullMessageWithException() throws MQBrokerException, RemotingException, InterruptedException, MQClientException { + when(processQueue.getMsgCount()).thenReturn(new AtomicLong(2)); + when(processQueue.getMsgSize()).thenReturn(new AtomicLong(3 * 1024 * 1024)); + when(pullRequest.getMessageQueue()).thenReturn(createMessageQueue()); + when(pullRequest.getProcessQueue()).thenReturn(processQueue); + defaultMQPushConsumerImpl.setServiceState(ServiceState.RUNNING); + when(defaultMQPushConsumer.getPullThresholdForQueue()).thenReturn(3); + when(defaultMQPushConsumer.getPullThresholdSizeForQueue()).thenReturn(10); + doAnswer(invocation -> { + PullCallback callback = invocation.getArgument(12); + callback.onException(new RuntimeException("exception")); + return null; + }).when(pullAPIWrapper).pullKernelImpl( + any(MessageQueue.class), + any(), + any(), + anyLong(), + anyLong(), + anyInt(), + anyInt(), + anyInt(), + anyLong(), + anyLong(), + anyLong(), + any(CommunicationMode.class), + any(PullCallback.class)); + defaultMQPushConsumerImpl.pullMessage(pullRequest); + } + + @Test + public void testPopMessageWithFound() throws RemotingException, InterruptedException, MQClientException { + when(popRequest.getPopProcessQueue()).thenReturn(popProcessQueue); + when(popRequest.getMessageQueue()).thenReturn(createMessageQueue()); + when(popRequest.getConsumerGroup()).thenReturn(defaultGroup); + defaultMQPushConsumerImpl.setServiceState(ServiceState.RUNNING); + ConcurrentMap subscriptionDataMap = new ConcurrentHashMap<>(); + SubscriptionData subscriptionData = new SubscriptionData(); + subscriptionData.setTagsSet(Collections.singleton("*")); + subscriptionDataMap.put(defaultTopic, subscriptionData); + when(rebalanceImpl.getSubscriptionInner()).thenReturn(subscriptionDataMap); + doAnswer(invocation -> { + PopCallback callback = invocation.getArgument(5); + PopResult popResult = mock(PopResult.class); + when(popResult.getPopStatus()).thenReturn(PopStatus.FOUND); + when(popResult.getMsgFoundList()).thenReturn(Collections.singletonList(createMessageExt())); + callback.onSuccess(popResult); + return null; + }).when(pullAPIWrapper).popAsync( + any(MessageQueue.class), + anyLong(), + anyInt(), + any(), + anyLong(), + any(PopCallback.class), + anyBoolean(), + anyInt(), + anyBoolean(), + any(), + any()); + defaultMQPushConsumerImpl.popMessage(popRequest); + } + + @Test + public void testPopMessageWithException() throws RemotingException, InterruptedException, MQClientException { + when(popRequest.getPopProcessQueue()).thenReturn(popProcessQueue); + when(popRequest.getMessageQueue()).thenReturn(createMessageQueue()); + when(popRequest.getConsumerGroup()).thenReturn(defaultGroup); + defaultMQPushConsumerImpl.setServiceState(ServiceState.RUNNING); + ConcurrentMap subscriptionDataMap = new ConcurrentHashMap<>(); + SubscriptionData subscriptionData = new SubscriptionData(); + subscriptionData.setTagsSet(Collections.singleton("*")); + subscriptionDataMap.put(defaultTopic, subscriptionData); + when(rebalanceImpl.getSubscriptionInner()).thenReturn(subscriptionDataMap); + doAnswer(invocation -> { + PopCallback callback = invocation.getArgument(5); + callback.onException(new RuntimeException("exception")); + return null; + }).when(pullAPIWrapper).popAsync( + any(MessageQueue.class), + anyLong(), + anyInt(), + any(), + anyLong(), + any(PopCallback.class), + anyBoolean(), + anyInt(), + anyBoolean(), + any(), + any()); + defaultMQPushConsumerImpl.popMessage(popRequest); + } + + @Test + public void testPopMessageWithNoNewMsg() throws RemotingException, InterruptedException, MQClientException { + when(popRequest.getPopProcessQueue()).thenReturn(popProcessQueue); + when(popRequest.getMessageQueue()).thenReturn(createMessageQueue()); + when(popRequest.getConsumerGroup()).thenReturn(defaultGroup); + defaultMQPushConsumerImpl.setServiceState(ServiceState.RUNNING); + ConcurrentMap subscriptionDataMap = new ConcurrentHashMap<>(); + SubscriptionData subscriptionData = new SubscriptionData(); + subscriptionData.setTagsSet(Collections.singleton("*")); + subscriptionDataMap.put(defaultTopic, subscriptionData); + when(rebalanceImpl.getSubscriptionInner()).thenReturn(subscriptionDataMap); + doAnswer(invocation -> { + PopCallback callback = invocation.getArgument(5); + PopResult popResult = mock(PopResult.class); + when(popResult.getPopStatus()).thenReturn(PopStatus.NO_NEW_MSG); + callback.onSuccess(popResult); + return null; + }).when(pullAPIWrapper).popAsync( + any(MessageQueue.class), + anyLong(), + anyInt(), + any(), + anyLong(), + any(PopCallback.class), + anyBoolean(), + anyInt(), + anyBoolean(), + any(), + any()); + defaultMQPushConsumerImpl.popMessage(popRequest); + } + + @Test + public void testPopMessageWithPollingFull() throws RemotingException, InterruptedException, MQClientException { + when(popRequest.getPopProcessQueue()).thenReturn(popProcessQueue); + when(popRequest.getMessageQueue()).thenReturn(createMessageQueue()); + when(popRequest.getConsumerGroup()).thenReturn(defaultGroup); + defaultMQPushConsumerImpl.setServiceState(ServiceState.RUNNING); + ConcurrentMap subscriptionDataMap = new ConcurrentHashMap<>(); + SubscriptionData subscriptionData = new SubscriptionData(); + subscriptionData.setTagsSet(Collections.singleton("*")); + subscriptionDataMap.put(defaultTopic, subscriptionData); + when(rebalanceImpl.getSubscriptionInner()).thenReturn(subscriptionDataMap); + doAnswer(invocation -> { + PopCallback callback = invocation.getArgument(5); + PopResult popResult = mock(PopResult.class); + when(popResult.getPopStatus()).thenReturn(PopStatus.POLLING_FULL); + callback.onSuccess(popResult); + return null; + }).when(pullAPIWrapper).popAsync(any( + MessageQueue.class), + anyLong(), + anyInt(), + any(), + anyLong(), + any(PopCallback.class), + anyBoolean(), + anyInt(), + anyBoolean(), + any(), + any()); + defaultMQPushConsumerImpl.popMessage(popRequest); + } + + @Test + public void testPopMessageWithStateNotOk() { + when(popRequest.getPopProcessQueue()).thenReturn(popProcessQueue); + defaultMQPushConsumerImpl.popMessage(popRequest); + } + + @Test + public void testPopMessageWithIsPause() { + when(popRequest.getPopProcessQueue()).thenReturn(popProcessQueue); + defaultMQPushConsumerImpl.setServiceState(ServiceState.RUNNING); + defaultMQPushConsumerImpl.setPause(true); + defaultMQPushConsumerImpl.popMessage(popRequest); + } + + @Test + public void testPopMessageWithWaiAckMsgCountFlowControl() { + when(popProcessQueue.getWaiAckMsgCount()).thenReturn(2); + when(popRequest.getPopProcessQueue()).thenReturn(popProcessQueue); + when(defaultMQPushConsumer.getPopThresholdForQueue()).thenReturn(1); + defaultMQPushConsumerImpl.setServiceState(ServiceState.RUNNING); + defaultMQPushConsumerImpl.popMessage(popRequest); + } + + @Test + public void testPopMessageWithSubscriptionDataIsNull() throws RemotingException, InterruptedException, MQClientException { + when(popProcessQueue.getWaiAckMsgCount()).thenReturn(2); + when(popRequest.getPopProcessQueue()).thenReturn(popProcessQueue); + when(popRequest.getMessageQueue()).thenReturn(createMessageQueue()); + defaultMQPushConsumerImpl.setServiceState(ServiceState.RUNNING); + when(defaultMQPushConsumer.getPopThresholdForQueue()).thenReturn(3); + defaultMQPushConsumerImpl.popMessage(popRequest); + verify(pullAPIWrapper).popAsync(any(MessageQueue.class), + eq(60000L), + eq(0), + any(), + eq(15000L), + any(PopCallback.class), + eq(true), + eq(0), + eq(false), + any(), + any()); + } + + @Test + public void testQueryMessage() throws InterruptedException, MQClientException { + assertNull(defaultMQPushConsumerImpl.queryMessage(defaultTopic, defaultKey, 1, 0, 1)); + } + + @Test + public void testQueryMessageByUniqKey() throws InterruptedException, MQClientException { + assertNull(defaultMQPushConsumerImpl.queryMessageByUniqKey(defaultTopic, defaultKey)); + } + + @Test + public void testSendMessageBack() throws InterruptedException, MQClientException, MQBrokerException, RemotingException { + when(mQClientFactory.findBrokerAddressInPublish(anyString())).thenReturn(defaultBrokerAddr); + defaultMQPushConsumerImpl.sendMessageBack(createMessageExt(), 1, createMessageQueue()); + verify(mqClientAPIImpl).consumerSendMessageBack( + eq(defaultBrokerAddr), + eq(defaultBroker), + any(MessageExt.class), + any(), + eq(1), + eq(5000L), + eq(0)); + } + + @Test + public void testAckAsync() throws MQBrokerException, RemotingException, InterruptedException { + doAnswer(invocation -> { + AckCallback callback = invocation.getArgument(2); + AckResult result = mock(AckResult.class); + when(result.getStatus()).thenReturn(AckStatus.OK); + callback.onSuccess(result); + return null; + }).when(mqClientAPIImpl).ackMessageAsync(any(), + anyLong(), + any(AckCallback.class), + any(AckMessageRequestHeader.class)); + defaultMQPushConsumerImpl.ackAsync(createMessageExt(), defaultGroup); + verify(mqClientAPIImpl).ackMessageAsync(eq(defaultBrokerAddr), + eq(3000L), + any(AckCallback.class), + any(AckMessageRequestHeader.class)); + } + + @Test + public void testChangePopInvisibleTimeAsync() throws MQBrokerException, RemotingException, InterruptedException, MQClientException { + AckCallback callback = mock(AckCallback.class); + String extraInfo = createMessageExt().getProperty(MessageConst.PROPERTY_POP_CK); + defaultMQPushConsumerImpl.changePopInvisibleTimeAsync(defaultTopic, defaultGroup, extraInfo, defaultTimeout, callback); + verify(mqClientAPIImpl).changeInvisibleTimeAsync(eq(defaultBroker), + eq(defaultBrokerAddr), + any(ChangeInvisibleTimeRequestHeader.class), + eq(defaultTimeout), + any(AckCallback.class)); + } + + @Test + public void testShutdown() { + defaultMQPushConsumerImpl.setServiceState(ServiceState.RUNNING); + defaultMQPushConsumerImpl.shutdown(); + assertEquals(ServiceState.SHUTDOWN_ALREADY, defaultMQPushConsumerImpl.getServiceState()); + } + + @Test + public void testSubscribe() throws MQClientException { + defaultMQPushConsumerImpl.subscribe(defaultTopic, "fullClassname", "filterClassSource"); + RebalanceImpl actual = defaultMQPushConsumerImpl.getRebalanceImpl(); + assertEquals(1, actual.getSubscriptionInner().size()); + } + + @Test + public void testSubscribeByMessageSelector() throws MQClientException { + MessageSelector messageSelector = mock(MessageSelector.class); + defaultMQPushConsumerImpl.subscribe(defaultTopic, messageSelector); + RebalanceImpl actual = defaultMQPushConsumerImpl.getRebalanceImpl(); + assertEquals(1, actual.getSubscriptionInner().size()); + } + + @Test + public void testSuspend() { + defaultMQPushConsumerImpl.suspend(); + assertTrue(defaultMQPushConsumerImpl.isPause()); + } + + @Test + public void testViewMessage() throws InterruptedException, MQClientException, MQBrokerException, RemotingException { + assertNull(defaultMQPushConsumerImpl.viewMessage(defaultTopic, createMessageExt().getMsgId())); + } + + @Test + public void testResetOffsetByTimeStamp() throws MQClientException { + ConcurrentMap subscriptionDataMap = new ConcurrentHashMap<>(); + subscriptionDataMap.put(defaultTopic, new SubscriptionData()); + when(rebalanceImpl.getSubscriptionInner()).thenReturn(subscriptionDataMap); + defaultMQPushConsumerImpl.resetOffsetByTimeStamp(System.currentTimeMillis()); + verify(mQClientFactory).resetOffset(eq(defaultTopic), any(), any()); + } + + @Test + public void testSearchOffset() throws MQClientException { + assertEquals(0, defaultMQPushConsumerImpl.searchOffset(createMessageQueue(), System.currentTimeMillis())); + } + + @Test + public void testQueryConsumeTimeSpan() throws InterruptedException, MQClientException, MQBrokerException, RemotingException { + TopicRouteData topicRouteData = new TopicRouteData(); + topicRouteData.getBrokerDatas().add(createBrokerData()); + when(mqClientAPIImpl.getTopicRouteInfoFromNameServer(any(), anyLong())).thenReturn(topicRouteData); + List actual = defaultMQPushConsumerImpl.queryConsumeTimeSpan(defaultTopic); + assertNotNull(actual); + assertEquals(0, actual.size()); + } + + @Test + public void testTryResetPopRetryTopic() { + TopicRouteData topicRouteData = new TopicRouteData(); + topicRouteData.getBrokerDatas().add(createBrokerData()); + MessageExt messageExt = createMessageExt(); + List msgs = new ArrayList<>(); + messageExt.setTopic(MixAll.RETRY_GROUP_TOPIC_PREFIX + defaultGroup + "_" + defaultTopic); + msgs.add(messageExt); + defaultMQPushConsumerImpl.tryResetPopRetryTopic(msgs, defaultGroup); + assertEquals(defaultTopic, msgs.get(0).getTopic()); + } + + @Test + public void testGetPopDelayLevel() { + int[] actual = defaultMQPushConsumerImpl.getPopDelayLevel(); + int[] expected = new int[]{10, 30, 60, 120, 180, 240, 300, 360, 420, 480, 540, 600, 1200, 1800, 3600, 7200}; + assertArrayEquals(expected, actual); + } + + @Test + public void testGetMessageQueueListener() { + assertNull(defaultMQPushConsumerImpl.getMessageQueueListener()); + } + + @Test + public void testConsumerRunningInfo() { + ConcurrentMap processQueueMap = new ConcurrentHashMap<>(); + ConcurrentMap popProcessQueueMap = new ConcurrentHashMap<>(); + processQueueMap.put(createMessageQueue(), new ProcessQueue()); + popProcessQueueMap.put(createMessageQueue(), new PopProcessQueue()); + when(rebalanceImpl.getProcessQueueTable()).thenReturn(processQueueMap); + when(rebalanceImpl.getPopProcessQueueTable()).thenReturn(popProcessQueueMap); + ConsumerRunningInfo actual = defaultMQPushConsumerImpl.consumerRunningInfo(); + assertNotNull(actual); + assertEquals(1, actual.getSubscriptionSet().size()); + assertEquals(defaultTopic, actual.getSubscriptionSet().iterator().next().getTopic()); + assertEquals(1, actual.getMqTable().size()); + assertEquals(1, actual.getMqPopTable().size()); + assertEquals(1, actual.getStatusTable().size()); + } + + private BrokerData createBrokerData() { + BrokerData result = new BrokerData(); + HashMap brokerAddrMap = new HashMap<>(); + brokerAddrMap.put(MixAll.MASTER_ID, defaultBrokerAddr); + result.setBrokerAddrs(brokerAddrMap); + result.setBrokerName(defaultBroker); + return result; + } + + private MessageQueue createMessageQueue() { + MessageQueue result = new MessageQueue(); + result.setQueueId(0); + result.setBrokerName(defaultBroker); + result.setTopic(defaultTopic); + return result; + } + + private MessageExt createMessageExt() { + MessageExt result = new MessageExt(); + result.setBody("body".getBytes(StandardCharsets.UTF_8)); + result.setTopic(defaultTopic); + result.setBrokerName(defaultBroker); + result.putUserProperty("key", "value"); + result.getProperties().put(MessageConst.PROPERTY_PRODUCER_GROUP, defaultGroup); + result.getProperties().put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, "TX1"); + long curTime = System.currentTimeMillis(); + result.setBornTimestamp(curTime - 1000); + String popProps = String.format("%d %d %d %d %d %s %d %d %d", curTime, curTime, curTime, curTime, curTime, defaultBroker, 1, 0L, 1L); + result.getProperties().put(MessageConst.PROPERTY_POP_CK, popProps); + result.setKeys("keys"); + result.setTags("*"); + SocketAddress bornHost = new InetSocketAddress("127.0.0.1", 12911); + SocketAddress storeHost = new InetSocketAddress("127.0.0.1", 10911); + result.setBornHost(bornHost); + result.setStoreHost(storeHost); + return result; + } +} 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 new file mode 100644 index 0000000..dd7ffa7 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ProcessQueueTest.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.client.impl.consumer; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.TreeMap; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.remoting.exception.RemotingException; +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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +@RunWith(MockitoJUnitRunner.class) +public class ProcessQueueTest { + + @Test + public void testCachedMessageCount() { + ProcessQueue pq = new ProcessQueue(); + + pq.putMessage(createMessageList()); + + assertThat(pq.getMsgCount().get()).isEqualTo(100); + + pq.takeMessages(10); + pq.commit(); + + assertThat(pq.getMsgCount().get()).isEqualTo(90); + + pq.removeMessage(Collections.singletonList(pq.getMsgTreeMap().lastEntry().getValue())); + assertThat(pq.getMsgCount().get()).isEqualTo(89); + } + + @Test + public void testCachedMessageSize() { + ProcessQueue pq = new ProcessQueue(); + + pq.putMessage(createMessageList()); + + assertThat(pq.getMsgSize().get()).isEqualTo(100 * 123); + + pq.takeMessages(10); + pq.commit(); + + assertThat(pq.getMsgSize().get()).isEqualTo(90 * 123); + + pq.removeMessage(Collections.singletonList(pq.getMsgTreeMap().lastEntry().getValue())); + 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() throws IllegalAccessException { + ProcessQueue pq = new ProcessQueue(); + pq.putMessage(createMessageList(102400)); + + ProcessQueueInfo processQueueInfo = new ProcessQueueInfo(); + pq.fillProcessQueueInfo(processQueueInfo); + + assertThat(processQueueInfo.getCachedMsgSizeInMiB()).isEqualTo(12); + + pq.takeMessages(10000); + pq.commit(); + pq.fillProcessQueueInfo(processQueueInfo); + assertThat(processQueueInfo.getCachedMsgSizeInMiB()).isEqualTo(10); + + pq.takeMessages(10000); + pq.commit(); + pq.fillProcessQueueInfo(processQueueInfo); + assertThat(processQueueInfo.getCachedMsgSizeInMiB()).isEqualTo(9); + + pq.takeMessages(80000); + pq.commit(); + pq.fillProcessQueueInfo(processQueueInfo); + assertThat(processQueueInfo.getCachedMsgSizeInMiB()).isEqualTo(0); + + TreeMap consumingMsgOrderlyTreeMap = new TreeMap<>(); + consumingMsgOrderlyTreeMap.put(0L, createMessageList(1).get(0)); + FieldUtils.writeDeclaredField(pq, "consumingMsgOrderlyTreeMap", consumingMsgOrderlyTreeMap, true); + pq.fillProcessQueueInfo(processQueueInfo); + assertEquals(0, processQueueInfo.getTransactionMsgMinOffset()); + assertEquals(0, processQueueInfo.getTransactionMsgMaxOffset()); + assertEquals(1, processQueueInfo.getTransactionMsgCount()); + } + + @Test + public void testPopRequest() throws MQBrokerException, RemotingException, InterruptedException, MQClientException { + ProcessQueue processQueue = createProcessQueue(); + MessageExt messageExt = createMessageList(1).get(0); + messageExt.getProperties().put(MessageConst.PROPERTY_CONSUME_START_TIMESTAMP, System.currentTimeMillis() - 20 * 60 * 1000L + ""); + processQueue.getMsgTreeMap().put(0L, messageExt); + DefaultMQPushConsumer pushConsumer = mock(DefaultMQPushConsumer.class); + processQueue.cleanExpiredMsg(pushConsumer); + verify(pushConsumer).sendMessageBack(any(MessageExt.class), eq(3)); + } + + @Test + public void testRollback() throws IllegalAccessException { + ProcessQueue processQueue = createProcessQueue(); + processQueue.rollback(); + Field consumingMsgOrderlyTreeMapField = FieldUtils.getDeclaredField(processQueue.getClass(), "consumingMsgOrderlyTreeMap", true); + TreeMap consumingMsgOrderlyTreeMap = (TreeMap) consumingMsgOrderlyTreeMapField.get(processQueue); + assertEquals(0, consumingMsgOrderlyTreeMap.size()); + } + + @Test + public void testHasTempMessage() { + ProcessQueue processQueue = createProcessQueue(); + assertFalse(processQueue.hasTempMessage()); + } + + @Test + public void testProcessQueue() { + ProcessQueue processQueue1 = createProcessQueue(); + ProcessQueue processQueue2 = createProcessQueue(); + assertEquals(processQueue1.getMsgAccCnt(), processQueue2.getMsgAccCnt()); + assertEquals(processQueue1.getTryUnlockTimes(), processQueue2.getTryUnlockTimes()); + assertEquals(processQueue1.getLastPullTimestamp(), processQueue2.getLastPullTimestamp()); + } + + private ProcessQueue createProcessQueue() { + ProcessQueue result = new ProcessQueue(); + result.setMsgAccCnt(1); + result.incTryUnlockTimes(); + return result; + } + + private List createMessageList() { + return createMessageList(100); + } + + private List createMessageList(int count) { + List result = new ArrayList<>(); + for (int i = 0; i < count; i++) { + MessageExt messageExt = new MessageExt(); + messageExt.setQueueOffset(i); + messageExt.setBody(new byte[123]); + messageExt.setKeys("keys" + i); + messageExt.getProperties().put(MessageConst.PROPERTY_CONSUME_START_TIMESTAMP, System.currentTimeMillis() + ""); + result.add(messageExt); + } + return result; + } +} diff --git a/client/src/test/java/org/apache/rocketmq/client/impl/consumer/PullAPIWrapperTest.java b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/PullAPIWrapperTest.java new file mode 100644 index 0000000..2ffa8f4 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/PullAPIWrapperTest.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 org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.client.ClientConfig; +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; +import org.apache.rocketmq.client.exception.MQClientException; +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.MQClientAPIImpl; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +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.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.common.sysflag.PullSysFlag; +import org.apache.rocketmq.remoting.exception.RemotingException; +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.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +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.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +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 PullAPIWrapperTest { + + @Mock + private MQClientInstance mQClientFactory; + + @Mock + private MQClientAPIImpl mqClientAPIImpl; + + private PullAPIWrapper pullAPIWrapper; + + private final String defaultGroup = "defaultGroup"; + + private final String defaultBroker = "defaultBroker"; + + private final String defaultTopic = "defaultTopic"; + + private final String defaultBrokerAddr = "127.0.0.1:10911"; + + private final long defaultTimeout = 3000L; + + @Before + public void init() throws Exception { + ClientConfig clientConfig = mock(ClientConfig.class); + when(mQClientFactory.getClientConfig()).thenReturn(clientConfig); + MQClientAPIImpl mqClientAPIImpl = mock(MQClientAPIImpl.class); + when(mQClientFactory.getMQClientAPIImpl()).thenReturn(mqClientAPIImpl); + when(mQClientFactory.getTopicRouteTable()).thenReturn(createTopicRouteTable()); + FindBrokerResult findBrokerResult = mock(FindBrokerResult.class); + when(findBrokerResult.getBrokerAddr()).thenReturn(defaultBrokerAddr); + when(mQClientFactory.findBrokerAddressInSubscribe(any(), anyLong(), anyBoolean())).thenReturn(findBrokerResult); + pullAPIWrapper = new PullAPIWrapper(mQClientFactory, defaultGroup, false); + ArrayList filterMessageHookList = new ArrayList<>(); + filterMessageHookList.add(mock(FilterMessageHook.class)); + FieldUtils.writeDeclaredField(pullAPIWrapper, "filterMessageHookList", filterMessageHookList, true); + } + + @Test + public void testProcessPullResult() throws Exception { + PullResultExt pullResult = mock(PullResultExt.class); + when(pullResult.getPullStatus()).thenReturn(PullStatus.FOUND); + when(pullResult.getMessageBinary()).thenReturn(MessageDecoder.encode(createMessageExt(), false)); + SubscriptionData subscriptionData = mock(SubscriptionData.class); + PullResult actual = pullAPIWrapper.processPullResult(createMessageQueue(), pullResult, subscriptionData); + assertNotNull(actual); + assertEquals(0, actual.getNextBeginOffset()); + assertEquals(0, actual.getMsgFoundList().size()); + } + + @Test + public void testExecuteHook() throws IllegalAccessException { + FilterMessageContext filterMessageContext = mock(FilterMessageContext.class); + ArrayList filterMessageHookList = new ArrayList<>(); + FilterMessageHook filterMessageHook = mock(FilterMessageHook.class); + filterMessageHookList.add(filterMessageHook); + FieldUtils.writeDeclaredField(pullAPIWrapper, "filterMessageHookList", filterMessageHookList, true); + pullAPIWrapper.executeHook(filterMessageContext); + verify(filterMessageHook, times(1)).filterMessage(any(FilterMessageContext.class)); + } + + @Test + public void testPullKernelImpl() throws Exception { + PullCallback pullCallback = mock(PullCallback.class); + when(mQClientFactory.getMQClientAPIImpl()).thenReturn(mqClientAPIImpl); + PullResult actual = pullAPIWrapper.pullKernelImpl(createMessageQueue(), + "", + "", + 1L, + 1L, + 1, + 1, + PullSysFlag.buildSysFlag(false, false, false, true), + 1L, + System.currentTimeMillis(), + defaultTimeout, CommunicationMode.ASYNC, pullCallback); + assertNull(actual); + verify(mqClientAPIImpl, times(1)).pullMessage(eq(defaultBroker), + any(PullMessageRequestHeader.class), + eq(defaultTimeout), + any(CommunicationMode.class), + any(PullCallback.class)); + } + + @Test + public void testSetConnectBrokerByUser() { + pullAPIWrapper.setConnectBrokerByUser(true); + assertTrue(pullAPIWrapper.isConnectBrokerByUser()); + } + + @Test + public void testRandomNum() { + int randomNum = pullAPIWrapper.randomNum(); + assertTrue(randomNum > 0); + } + + @Test + public void testSetDefaultBrokerId() { + pullAPIWrapper.setDefaultBrokerId(MixAll.MASTER_ID); + assertEquals(MixAll.MASTER_ID, pullAPIWrapper.getDefaultBrokerId()); + } + + @Test + public void testPopAsync() throws RemotingException, InterruptedException, MQClientException { + PopCallback popCallback = mock(PopCallback.class); + when(mQClientFactory.getMQClientAPIImpl()).thenReturn(mqClientAPIImpl); + pullAPIWrapper.popAsync(createMessageQueue(), + System.currentTimeMillis(), + 1, + defaultGroup, + defaultTimeout, + popCallback, + true, + 1, + false, + "", + ""); + verify(mqClientAPIImpl, times(1)).popMessageAsync(eq(defaultBroker), + eq(defaultBrokerAddr), + any(PopMessageRequestHeader.class), + eq(13000L), + any(PopCallback.class)); + } + + private ConcurrentMap createTopicRouteTable() { + TopicRouteData topicRouteData = new TopicRouteData(); + List brokerDatas = new ArrayList<>(); + BrokerData brokerData = new BrokerData(); + brokerData.setBrokerName(defaultBroker); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(MixAll.MASTER_ID, defaultBroker); + brokerData.setBrokerAddrs(brokerAddrs); + brokerDatas.add(brokerData); + topicRouteData.setBrokerDatas(brokerDatas); + HashMap> filterServerTable = new HashMap<>(); + List filterServers = new ArrayList<>(); + filterServers.add(defaultBroker); + filterServerTable.put(defaultBrokerAddr, filterServers); + topicRouteData.setFilterServerTable(filterServerTable); + ConcurrentMap result = new ConcurrentHashMap<>(); + result.put(defaultTopic, topicRouteData); + return result; + } + + private MessageQueue createMessageQueue() { + MessageQueue result = new MessageQueue(); + result.setQueueId(0); + result.setBrokerName(defaultBroker); + result.setTopic(defaultTopic); + return result; + } + + private MessageExt createMessageExt() { + MessageExt result = new MessageExt(); + result.setBody("body".getBytes(StandardCharsets.UTF_8)); + result.setTopic(defaultTopic); + result.setBrokerName(defaultBroker); + result.putUserProperty("key", "value"); + result.getProperties().put(MessageConst.PROPERTY_PRODUCER_GROUP, defaultGroup); + result.getProperties().put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, "TX1"); + long curTime = System.currentTimeMillis(); + result.setBornTimestamp(curTime - 1000); + result.getProperties().put(MessageConst.PROPERTY_POP_CK, curTime + " " + curTime + " " + curTime + " " + curTime); + result.setKeys("keys"); + result.setSysFlag(MessageSysFlag.INNER_BATCH_FLAG); + result.setSysFlag(result.getSysFlag() | MessageSysFlag.NEED_UNWRAP_FLAG); + SocketAddress bornHost = new InetSocketAddress("127.0.0.1", 12911); + SocketAddress storeHost = new InetSocketAddress("127.0.0.1", 10911); + result.setBornHost(bornHost); + result.setStoreHost(storeHost); + return result; + } +} diff --git a/client/src/test/java/org/apache/rocketmq/client/impl/consumer/PullMessageServiceTest.java b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/PullMessageServiceTest.java new file mode 100644 index 0000000..73fa4e9 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/PullMessageServiceTest.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.client.impl.consumer; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.common.message.MessageRequestMode; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +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 PullMessageServiceTest { + + @Mock + private MQClientInstance mQClientFactory; + + @Mock + private ScheduledExecutorService executorService; + + private PullMessageService pullMessageService; + + private final long defaultTimeout = 3000L; + + private final String defaultGroup = "defaultGroup"; + + @Before + public void init() throws Exception { + pullMessageService = new PullMessageService(mQClientFactory); + FieldUtils.writeDeclaredField(pullMessageService, "scheduledExecutorService", executorService, true); + pullMessageService.start(); + } + + @Test + public void testProcessPullResult() { + PopRequest popRequest = mock(PopRequest.class); + pullMessageService.executePopPullRequestLater(popRequest, defaultTimeout); + pullMessageService.makeStop(); + pullMessageService.executePopPullRequestLater(popRequest, defaultTimeout); + verify(executorService, times(1)) + .schedule(any(Runnable.class), + eq(defaultTimeout), + eq(TimeUnit.MILLISECONDS)); + } + + @Test + public void testExecutePopPullRequestImmediately() throws IllegalAccessException, InterruptedException { + PopRequest popRequest = mock(PopRequest.class); + LinkedBlockingQueue messageRequestQueue = mock(LinkedBlockingQueue.class); + FieldUtils.writeDeclaredField(pullMessageService, "messageRequestQueue", messageRequestQueue, true); + pullMessageService.executePopPullRequestImmediately(popRequest); + verify(messageRequestQueue, times(1)).put(any(PopRequest.class)); + } + + @Test + public void testExecuteTaskLater() { + Runnable runnable = mock(Runnable.class); + pullMessageService.executeTaskLater(runnable, defaultTimeout); + pullMessageService.makeStop(); + pullMessageService.executeTaskLater(runnable, defaultTimeout); + verify(executorService, times(1)) + .schedule(any(Runnable.class), + eq(defaultTimeout), + eq(TimeUnit.MILLISECONDS)); + } + + @Test + public void testExecuteTask() { + Runnable runnable = mock(Runnable.class); + pullMessageService.executeTask(runnable); + pullMessageService.makeStop(); + pullMessageService.executeTask(runnable); + verify(executorService, times(1)).execute(any(Runnable.class)); + } + + @Test + public void testGetScheduledExecutorService() { + assertEquals(executorService, pullMessageService.getScheduledExecutorService()); + } + + @Test + public void testRun() throws InterruptedException, IllegalAccessException { + LinkedBlockingQueue messageRequestQueue = new LinkedBlockingQueue<>(); + PopRequest popRequest = mock(PopRequest.class); + when(popRequest.getMessageRequestMode()).thenReturn(MessageRequestMode.POP); + when(popRequest.getConsumerGroup()).thenReturn(defaultGroup); + messageRequestQueue.put(popRequest); + DefaultMQPushConsumerImpl defaultMQPushConsumerImpl = mock(DefaultMQPushConsumerImpl.class); + when(mQClientFactory.selectConsumer(any())).thenReturn(defaultMQPushConsumerImpl); + FieldUtils.writeDeclaredField(pullMessageService, "messageRequestQueue", messageRequestQueue, true); + new Thread(() -> pullMessageService.run()).start(); + TimeUnit.SECONDS.sleep(1); + pullMessageService.makeStop(); + verify(mQClientFactory, times(1)).selectConsumer(eq(defaultGroup)); + verify(defaultMQPushConsumerImpl).popMessage(any(PopRequest.class)); + } + + @Test + public void testRunWithNullConsumer() throws InterruptedException, IllegalAccessException { + LinkedBlockingQueue messageRequestQueue = new LinkedBlockingQueue<>(); + PopRequest popRequest = mock(PopRequest.class); + when(popRequest.getMessageRequestMode()).thenReturn(MessageRequestMode.POP); + when(popRequest.getConsumerGroup()).thenReturn(defaultGroup); + messageRequestQueue.put(popRequest); + FieldUtils.writeDeclaredField(pullMessageService, "messageRequestQueue", messageRequestQueue, true); + new Thread(() -> pullMessageService.run()).start(); + TimeUnit.SECONDS.sleep(1); + pullMessageService.makeStop(); + verify(mQClientFactory, times(1)).selectConsumer(eq(defaultGroup)); + } +} 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 0000000..1dbda1c --- /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 new file mode 100644 index 0000000..f55b586 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/RebalancePushImplTest.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.client.impl.consumer; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +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.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.junit.MockitoJUnitRunner; + +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.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class RebalancePushImplTest { + @Spy + private DefaultMQPushConsumerImpl defaultMQPushConsumer = new DefaultMQPushConsumerImpl(new DefaultMQPushConsumer("RebalancePushImplTest"), null); + @Mock + private MQClientInstance mqClientInstance; + 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() { + RebalancePushImpl rebalancePush = new RebalancePushImpl(consumerGroup, MessageModel.CLUSTERING, + new AllocateMessageQueueAveragely(), mqClientInstance, defaultMQPushConsumer); + init(rebalancePush); + + // Just set pullThresholdForQueue + defaultMQPushConsumer.getDefaultMQPushConsumer().setPullThresholdForQueue(1024); + Set allocateResultSet = new HashSet<>(); + allocateResultSet.add(new MessageQueue(topic, "BrokerA", 0)); + allocateResultSet.add(new MessageQueue(topic, "BrokerA", 1)); + doRebalanceForcibly(rebalancePush, allocateResultSet); + assertThat(defaultMQPushConsumer.getDefaultMQPushConsumer().getPullThresholdForQueue()).isEqualTo(1024); + + // Set pullThresholdForTopic + defaultMQPushConsumer.getDefaultMQPushConsumer().setPullThresholdForTopic(1024); + doRebalanceForcibly(rebalancePush, allocateResultSet); + assertThat(defaultMQPushConsumer.getDefaultMQPushConsumer().getPullThresholdForQueue()).isEqualTo(512); + + // Change message queue allocate result + allocateResultSet.add(new MessageQueue(topic, "BrokerA", 2)); + doRebalanceForcibly(rebalancePush, allocateResultSet); + assertThat(defaultMQPushConsumer.getDefaultMQPushConsumer().getPullThresholdForQueue()).isEqualTo(341); + } + + private void doRebalanceForcibly(RebalancePushImpl rebalancePush, Set allocateResultSet) { + rebalancePush.topicSubscribeInfoTable.put(topic, allocateResultSet); + rebalancePush.doRebalance(false); + rebalancePush.messageQueueChanged(topic, allocateResultSet, allocateResultSet); + } + + private void init(final RebalancePushImpl rebalancePush) { + rebalancePush.getSubscriptionInner().putIfAbsent(topic, new SubscriptionData()); + + rebalancePush.subscriptionInner.putIfAbsent(topic, new SubscriptionData()); + + when(mqClientInstance.findConsumerIdList(anyString(), anyString())).thenReturn(Collections.singletonList(consumerGroup)); + when(mqClientInstance.getClientId()).thenReturn(consumerGroup); + when(defaultMQPushConsumer.getOffsetStore()).thenReturn(offsetStore); + } + + @Test + public void testMessageQueueChanged_SizeThreshold() { + RebalancePushImpl rebalancePush = new RebalancePushImpl(consumerGroup, MessageModel.CLUSTERING, + new AllocateMessageQueueAveragely(), mqClientInstance, defaultMQPushConsumer); + init(rebalancePush); + + // Just set pullThresholdSizeForQueue + defaultMQPushConsumer.getDefaultMQPushConsumer().setPullThresholdSizeForQueue(1024); + Set allocateResultSet = new HashSet<>(); + allocateResultSet.add(new MessageQueue(topic, "BrokerA", 0)); + allocateResultSet.add(new MessageQueue(topic, "BrokerA", 1)); + doRebalanceForcibly(rebalancePush, allocateResultSet); + assertThat(defaultMQPushConsumer.getDefaultMQPushConsumer().getPullThresholdSizeForQueue()).isEqualTo(1024); + + // Set pullThresholdSizeForTopic + defaultMQPushConsumer.getDefaultMQPushConsumer().setPullThresholdSizeForTopic(1024); + doRebalanceForcibly(rebalancePush, allocateResultSet); + assertThat(defaultMQPushConsumer.getDefaultMQPushConsumer().getPullThresholdSizeForQueue()).isEqualTo(512); + + // Change message queue allocate result + allocateResultSet.add(new MessageQueue(topic, "BrokerA", 2)); + doRebalanceForcibly(rebalancePush, allocateResultSet); + assertThat(defaultMQPushConsumer.getDefaultMQPushConsumer().getPullThresholdSizeForQueue()).isEqualTo(341); + } + + @Test + public void testMessageQueueChanged_ConsumerRuntimeInfo() throws MQClientException { + RebalancePushImpl rebalancePush = new RebalancePushImpl(consumerGroup, MessageModel.CLUSTERING, + new AllocateMessageQueueAveragely(), mqClientInstance, defaultMQPushConsumer); + init(rebalancePush); + + defaultMQPushConsumer.getDefaultMQPushConsumer().setPullThresholdSizeForQueue(1024); + defaultMQPushConsumer.getDefaultMQPushConsumer().setPullThresholdForQueue(1024); + Set allocateResultSet = new HashSet<>(); + allocateResultSet.add(new MessageQueue(topic, "BrokerA", 0)); + allocateResultSet.add(new MessageQueue(topic, "BrokerA", 1)); + doRebalanceForcibly(rebalancePush, allocateResultSet); + + defaultMQPushConsumer.setConsumeMessageService(new ConsumeMessageConcurrentlyService(defaultMQPushConsumer, null)); + assertThat(defaultMQPushConsumer.consumerRunningInfo().getProperties().get("pullThresholdSizeForQueue")).isEqualTo("1024"); + assertThat(defaultMQPushConsumer.consumerRunningInfo().getProperties().get("pullThresholdForQueue")).isEqualTo("1024"); + assertThat(defaultMQPushConsumer.consumerRunningInfo().getProperties().get("pullThresholdSizeForTopic")).isEqualTo("-1"); + assertThat(defaultMQPushConsumer.consumerRunningInfo().getProperties().get("pullThresholdForTopic")).isEqualTo("-1"); + + defaultMQPushConsumer.getDefaultMQPushConsumer().setPullThresholdSizeForTopic(1024); + defaultMQPushConsumer.getDefaultMQPushConsumer().setPullThresholdForTopic(1024); + doRebalanceForcibly(rebalancePush, allocateResultSet); + assertThat(defaultMQPushConsumer.consumerRunningInfo().getProperties().get("pullThresholdSizeForQueue")).isEqualTo("512"); + assertThat(defaultMQPushConsumer.consumerRunningInfo().getProperties().get("pullThresholdForQueue")).isEqualTo("512"); + assertThat(defaultMQPushConsumer.consumerRunningInfo().getProperties().get("pullThresholdSizeForTopic")).isEqualTo("1024"); + assertThat(defaultMQPushConsumer.consumerRunningInfo().getProperties().get("pullThresholdForTopic")).isEqualTo("1024"); + + // Change message queue allocate result + allocateResultSet.add(new MessageQueue(topic, "BrokerA", 2)); + doRebalanceForcibly(rebalancePush, allocateResultSet); + assertThat(defaultMQPushConsumer.consumerRunningInfo().getProperties().get("pullThresholdSizeForQueue")).isEqualTo("341"); + assertThat(defaultMQPushConsumer.consumerRunningInfo().getProperties().get("pullThresholdForQueue")).isEqualTo("341"); + assertThat(defaultMQPushConsumer.consumerRunningInfo().getProperties().get("pullThresholdSizeForTopic")).isEqualTo("1024"); + assertThat(defaultMQPushConsumer.consumerRunningInfo().getProperties().get("pullThresholdForTopic")).isEqualTo("1024"); + } + + @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 new file mode 100644 index 0000000..d71bc25 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/impl/factory/MQClientInstanceTest.java @@ -0,0 +1,504 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.factory; + +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.consumer.store.OffsetStore; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +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.ConsumeMessageService; +import org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl; +import org.apache.rocketmq.client.impl.consumer.MQConsumerInner; +import org.apache.rocketmq.client.impl.consumer.ProcessQueue; +import org.apache.rocketmq.client.impl.consumer.RebalanceImpl; +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.common.MixAll; +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.message.MessageQueueAssignment; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.remoting.RemotingClient; +import org.apache.rocketmq.remoting.common.HeartbeatV2Result; +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.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +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.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; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +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 MQClientInstanceTest { + + @Mock + private MQClientAPIImpl mQClientAPIImpl; + + @Mock + private RemotingClient remotingClient; + + @Mock + private ClientConfig clientConfig; + + private final MQClientInstance mqClientInstance = MQClientManager.getInstance().getOrCreateMQClientInstance(new ClientConfig()); + + private final String topic = "FooBar"; + + private final String group = "FooBarGroup"; + + private final String defaultBrokerAddr = "127.0.0.1:10911"; + + private final String defaultBroker = "BrokerA"; + + private final ConcurrentMap> brokerAddrTable = new ConcurrentHashMap<>(); + + private final ConcurrentMap consumerTable = new ConcurrentHashMap<>(); + + private final ConcurrentMap topicRouteTable = new ConcurrentHashMap<>(); + + @Before + public void init() throws Exception { + when(mQClientAPIImpl.getRemotingClient()).thenReturn(remotingClient); + FieldUtils.writeDeclaredField(mqClientInstance, "brokerAddrTable", brokerAddrTable, true); + FieldUtils.writeDeclaredField(mqClientInstance, "mQClientAPIImpl", mQClientAPIImpl, true); + FieldUtils.writeDeclaredField(mqClientInstance, "consumerTable", consumerTable, true); + FieldUtils.writeDeclaredField(mqClientInstance, "clientConfig", clientConfig, true); + FieldUtils.writeDeclaredField(mqClientInstance, "topicRouteTable", topicRouteTable, true); + } + + @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)); + assertThat(flag).isTrue(); + + flag = mqClientInstance.registerProducer(group, mock(DefaultMQProducerImpl.class)); + assertThat(flag).isFalse(); + + mqClientInstance.unregisterProducer(group); + flag = mqClientInstance.registerProducer(group, mock(DefaultMQProducerImpl.class)); + assertThat(flag).isTrue(); + } + + @Test + public void testRegisterConsumer() { + boolean flag = mqClientInstance.registerConsumer(group, mock(MQConsumerInner.class)); + assertThat(flag).isTrue(); + + flag = mqClientInstance.registerConsumer(group, mock(MQConsumerInner.class)); + assertThat(flag).isFalse(); + + mqClientInstance.unregisterConsumer(group); + flag = mqClientInstance.registerConsumer(group, mock(MQConsumerInner.class)); + 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)); + assertThat(flag).isTrue(); + + flag = mqClientInstance.registerAdminExt(group, mock(MQAdminExtInner.class)); + assertThat(flag).isFalse(); + + mqClientInstance.unregisterAdminExt(group); + flag = mqClientInstance.registerAdminExt(group, mock(MQAdminExtInner.class)); + assertThat(flag).isTrue(); + } + + @Test + public void testTopicRouteData2TopicPublishInfo() { + TopicPublishInfo actual = MQClientInstance.topicRouteData2TopicPublishInfo(topic, createTopicRouteData()); + assertThat(actual.isHaveTopicRouterInfo()).isFalse(); + assertThat(actual.getMessageQueueList().size()).isEqualTo(4); + } + + @Test + public void testTopicRouteData2TopicPublishInfoWithOrderTopicConf() { + TopicRouteData topicRouteData = createTopicRouteData(); + when(topicRouteData.getOrderTopicConf()).thenReturn("127.0.0.1:4"); + TopicPublishInfo actual = MQClientInstance.topicRouteData2TopicPublishInfo(topic, topicRouteData); + assertFalse(actual.isHaveTopicRouterInfo()); + assertEquals(4, actual.getMessageQueueList().size()); + } + + @Test + public void testTopicRouteData2TopicPublishInfoWithTopicQueueMappingByBroker() { + TopicRouteData topicRouteData = createTopicRouteData(); + when(topicRouteData.getTopicQueueMappingByBroker()).thenReturn(Collections.singletonMap(topic, new TopicQueueMappingInfo())); + TopicPublishInfo actual = MQClientInstance.topicRouteData2TopicPublishInfo(topic, topicRouteData); + assertFalse(actual.isHaveTopicRouterInfo()); + assertEquals(0, actual.getMessageQueueList().size()); + } + + @Test + public void testTopicRouteData2TopicSubscribeInfo() { + TopicRouteData topicRouteData = createTopicRouteData(); + when(topicRouteData.getTopicQueueMappingByBroker()).thenReturn(Collections.singletonMap(topic, new TopicQueueMappingInfo())); + Set actual = MQClientInstance.topicRouteData2TopicSubscribeInfo(topic, topicRouteData); + assertNotNull(actual); + assertEquals(0, actual.size()); + } + + @Test + public void testParseOffsetTableFromBroker() { + Map offsetTable = new HashMap<>(); + offsetTable.put(new MessageQueue(), 0L); + Map actual = mqClientInstance.parseOffsetTableFromBroker(offsetTable, "defaultNamespace"); + assertNotNull(actual); + assertEquals(1, actual.size()); + } + + @Test + public void testCheckClientInBroker() throws MQClientException, RemotingSendRequestException, RemotingConnectException, RemotingTimeoutException, InterruptedException { + doThrow(new MQClientException("checkClientInBroker exception", null)).when(mQClientAPIImpl).checkClientInBroker( + any(), + any(), + any(), + any(SubscriptionData.class), + anyLong()); + topicRouteTable.put(topic, createTopicRouteData()); + MQConsumerInner mqConsumerInner = createMQConsumerInner(); + mqConsumerInner.subscriptions().clear(); + SubscriptionData subscriptionData = new SubscriptionData(); + subscriptionData.setTopic(topic); + subscriptionData.setExpressionType("type"); + mqConsumerInner.subscriptions().add(subscriptionData); + consumerTable.put(group, mqConsumerInner); + Throwable thrown = assertThrows(MQClientException.class, mqClientInstance::checkClientInBroker); + assertTrue(thrown.getMessage().contains("checkClientInBroker exception")); + } + + @Test + public void testSendHeartbeatToBrokerV1() { + consumerTable.put(group, createMQConsumerInner()); + assertTrue(mqClientInstance.sendHeartbeatToBroker(0L, defaultBroker, defaultBrokerAddr)); + } + + @Test + public void testSendHeartbeatToBrokerV2() throws MQBrokerException, RemotingException, InterruptedException { + consumerTable.put(group, createMQConsumerInner()); + when(clientConfig.isUseHeartbeatV2()).thenReturn(true); + HeartbeatV2Result heartbeatV2Result = mock(HeartbeatV2Result.class); + when(heartbeatV2Result.isSupportV2()).thenReturn(true); + when(mQClientAPIImpl.sendHeartbeatV2(any(), any(HeartbeatData.class), anyLong())).thenReturn(heartbeatV2Result); + assertTrue(mqClientInstance.sendHeartbeatToBroker(0L, defaultBroker, defaultBrokerAddr)); + } + + @Test + public void testSendHeartbeatToAllBrokerWithLockV1() { + brokerAddrTable.put(defaultBroker, createBrokerAddrMap()); + consumerTable.put(group, createMQConsumerInner()); + assertTrue(mqClientInstance.sendHeartbeatToAllBrokerWithLock()); + } + + @Test + public void testSendHeartbeatToAllBrokerWithLockV2() { + brokerAddrTable.put(defaultBroker, createBrokerAddrMap()); + consumerTable.put(group, createMQConsumerInner()); + when(clientConfig.isUseHeartbeatV2()).thenReturn(true); + assertTrue(mqClientInstance.sendHeartbeatToAllBrokerWithLock()); + } + + @Test + public void testUpdateTopicRouteInfoFromNameServer() throws RemotingException, InterruptedException, MQClientException { + brokerAddrTable.put(defaultBroker, createBrokerAddrMap()); + consumerTable.put(group, createMQConsumerInner()); + DefaultMQProducer defaultMQProducer = mock(DefaultMQProducer.class); + TopicRouteData topicRouteData = createTopicRouteData(); + when(mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(anyLong())).thenReturn(topicRouteData); + assertFalse(mqClientInstance.updateTopicRouteInfoFromNameServer(topic, true, defaultMQProducer)); + } + + @Test + public void testFindBrokerAddressInAdmin() { + brokerAddrTable.put(defaultBroker, createBrokerAddrMap()); + consumerTable.put(group, createMQConsumerInner()); + FindBrokerResult actual = mqClientInstance.findBrokerAddressInAdmin(defaultBroker); + assertNotNull(actual); + assertEquals(defaultBrokerAddr, actual.getBrokerAddr()); + } + + @Test + public void testFindBrokerAddressInSubscribeWithOneBroker() throws IllegalAccessException { + brokerAddrTable.put(defaultBroker, createBrokerAddrMap()); + consumerTable.put(group, createMQConsumerInner()); + ConcurrentMap> brokerVersionTable = new ConcurrentHashMap<>(); + HashMap addressMap = new HashMap<>(); + addressMap.put(defaultBrokerAddr, 0); + brokerVersionTable.put(defaultBroker, addressMap); + FieldUtils.writeDeclaredField(mqClientInstance, "brokerVersionTable", brokerVersionTable, true); + FindBrokerResult actual = mqClientInstance.findBrokerAddressInSubscribe(defaultBroker, 1L, false); + assertNotNull(actual); + assertEquals(defaultBrokerAddr, actual.getBrokerAddr()); + } + + @Test + public void testFindConsumerIdList() { + topicRouteTable.put(topic, createTopicRouteData()); + brokerAddrTable.put(defaultBroker, createBrokerAddrMap()); + consumerTable.put(group, createMQConsumerInner()); + List actual = mqClientInstance.findConsumerIdList(topic, group); + assertNotNull(actual); + assertEquals(0, actual.size()); + } + + @Test + public void testQueryAssignment() throws MQBrokerException, RemotingException, InterruptedException { + topicRouteTable.put(topic, createTopicRouteData()); + brokerAddrTable.put(defaultBroker, createBrokerAddrMap()); + consumerTable.put(group, createMQConsumerInner()); + Set actual = mqClientInstance.queryAssignment(topic, group, "", MessageModel.CLUSTERING, 1000); + assertNotNull(actual); + assertEquals(0, actual.size()); + } + + @Test + public void testResetOffset() throws IllegalAccessException { + topicRouteTable.put(topic, createTopicRouteData()); + brokerAddrTable.put(defaultBroker, createBrokerAddrMap()); + consumerTable.put(group, createMQConsumerInner()); + Map offsetTable = new HashMap<>(); + offsetTable.put(createMessageQueue(), 0L); + mqClientInstance.resetOffset(topic, group, offsetTable); + Field consumerTableField = FieldUtils.getDeclaredField(mqClientInstance.getClass(), "consumerTable", true); + ConcurrentMap consumerTable = (ConcurrentMap) consumerTableField.get(mqClientInstance); + DefaultMQPushConsumerImpl consumer = (DefaultMQPushConsumerImpl) consumerTable.get(group); + verify(consumer).suspend(); + verify(consumer).resume(); + verify(consumer, times(1)) + .updateConsumeOffset( + any(MessageQueue.class), + eq(0L)); + } + + @Test + public void testGetConsumerStatus() { + topicRouteTable.put(topic, createTopicRouteData()); + brokerAddrTable.put(defaultBroker, createBrokerAddrMap()); + consumerTable.put(group, createMQConsumerInner()); + Map actual = mqClientInstance.getConsumerStatus(topic, group); + assertNotNull(actual); + assertEquals(0, actual.size()); + } + + @Test + public void testGetAnExistTopicRouteData() { + topicRouteTable.put(topic, createTopicRouteData()); + TopicRouteData actual = mqClientInstance.getAnExistTopicRouteData(topic); + assertNotNull(actual); + assertNotNull(actual.getQueueDatas()); + assertNotNull(actual.getBrokerDatas()); + } + + @Test + public void testConsumeMessageDirectly() { + consumerTable.put(group, createMQConsumerInner()); + assertNull(mqClientInstance.consumeMessageDirectly(createMessageExt(), group, defaultBroker)); + } + + @Test + public void testQueryTopicRouteData() { + consumerTable.put(group, createMQConsumerInner()); + topicRouteTable.put(topic, createTopicRouteData()); + TopicRouteData actual = mqClientInstance.queryTopicRouteData(topic); + assertNotNull(actual); + assertNotNull(actual.getQueueDatas()); + assertNotNull(actual.getBrokerDatas()); + } + + private MessageExt createMessageExt() { + MessageExt result = new MessageExt(); + result.setBody("body".getBytes(StandardCharsets.UTF_8)); + result.setTopic(topic); + result.setBrokerName(defaultBroker); + result.putUserProperty("key", "value"); + result.getProperties().put(MessageConst.PROPERTY_PRODUCER_GROUP, group); + result.getProperties().put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, "TX1"); + long curTime = System.currentTimeMillis(); + result.setBornTimestamp(curTime - 1000); + result.getProperties().put(MessageConst.PROPERTY_POP_CK, curTime + " " + curTime + " " + curTime + " " + curTime); + result.setKeys("keys"); + result.setSysFlag(MessageSysFlag.INNER_BATCH_FLAG); + result.setSysFlag(result.getSysFlag() | MessageSysFlag.NEED_UNWRAP_FLAG); + SocketAddress bornHost = new InetSocketAddress("127.0.0.1", 12911); + SocketAddress storeHost = new InetSocketAddress("127.0.0.1", 10911); + result.setBornHost(bornHost); + result.setStoreHost(storeHost); + return result; + } + + private MessageQueue createMessageQueue() { + MessageQueue result = new MessageQueue(); + result.setQueueId(0); + result.setBrokerName(defaultBroker); + result.setTopic(topic); + return result; + } + + private TopicRouteData createTopicRouteData() { + TopicRouteData result = mock(TopicRouteData.class); + when(result.getBrokerDatas()).thenReturn(createBrokerDatas()); + when(result.getQueueDatas()).thenReturn(createQueueDatas()); + return result; + } + + private HashMap createBrokerAddrMap() { + HashMap result = new HashMap<>(); + result.put(0L, defaultBrokerAddr); + return result; + } + + private MQConsumerInner createMQConsumerInner() { + DefaultMQPushConsumerImpl result = mock(DefaultMQPushConsumerImpl.class); + Set subscriptionDataSet = new HashSet<>(); + SubscriptionData subscriptionData = mock(SubscriptionData.class); + subscriptionDataSet.add(subscriptionData); + when(result.subscriptions()).thenReturn(subscriptionDataSet); + RebalanceImpl rebalanceImpl = mock(RebalanceImpl.class); + ConcurrentMap processQueueMap = new ConcurrentHashMap<>(); + ProcessQueue processQueue = new ProcessQueue(); + processQueueMap.put(createMessageQueue(), processQueue); + when(rebalanceImpl.getProcessQueueTable()).thenReturn(processQueueMap); + when(result.getRebalanceImpl()).thenReturn(rebalanceImpl); + OffsetStore offsetStore = mock(OffsetStore.class); + when(result.getOffsetStore()).thenReturn(offsetStore); + ConsumeMessageService consumeMessageService = mock(ConsumeMessageService.class); + when(result.getConsumeMessageService()).thenReturn(consumeMessageService); + return result; + } + + private List createQueueDatas() { + QueueData queueData = new QueueData(); + queueData.setBrokerName(defaultBroker); + queueData.setPerm(6); + queueData.setReadQueueNums(3); + queueData.setWriteQueueNums(4); + queueData.setTopicSysFlag(0); + return Collections.singletonList(queueData); + } + + private List createBrokerDatas() { + BrokerData brokerData = new BrokerData(); + brokerData.setBrokerName(defaultBroker); + String defaultCluster = "defaultCluster"; + brokerData.setCluster(defaultCluster); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(MixAll.MASTER_ID, defaultBrokerAddr); + brokerData.setBrokerAddrs(brokerAddrs); + return Collections.singletonList(brokerData); + } +} diff --git a/client/src/test/java/org/apache/rocketmq/client/impl/mqclient/MQClientAPIExtTest.java b/client/src/test/java/org/apache/rocketmq/client/impl/mqclient/MQClientAPIExtTest.java new file mode 100644 index 0000000..e2a29c9 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/impl/mqclient/MQClientAPIExtTest.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.client.impl.mqclient; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.utils.FutureUtils; +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; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.RecallMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.RecallMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateConsumerOffsetRequestHeader; +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; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; + +@RunWith(MockitoJUnitRunner.class) +public class MQClientAPIExtTest { + MQClientAPIExt mqClientAPIExt; + @Mock + NettyRemotingClient remotingClientMock; + + @Before + public void before() { + mqClientAPIExt = Mockito.spy(new MQClientAPIExt(new ClientConfig(), new NettyClientConfig(), null, null)); + Mockito.when(mqClientAPIExt.getRemotingClient()).thenReturn(remotingClientMock); + Mockito.when(remotingClientMock.invoke(anyString(), any(), anyLong())).thenReturn(FutureUtils.completeExceptionally(new RemotingTimeoutException("addr"))); + } + + @Test + public void sendMessageAsync() { + String topic = "test"; + Message msg = new Message(topic, "test".getBytes()); + SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setProducerGroup("test"); + requestHeader.setDefaultTopic("test"); + requestHeader.setDefaultTopicQueueNums(1); + requestHeader.setQueueId(0); + requestHeader.setSysFlag(0); + requestHeader.setBornTimestamp(0L); + requestHeader.setFlag(0); + requestHeader.setProperties("test"); + requestHeader.setReconsumeTimes(0); + requestHeader.setUnitMode(false); + requestHeader.setBatch(false); + CompletableFuture future = mqClientAPIExt.sendMessageAsync("127.0.0.1:10911", "test", msg, requestHeader, 10); + assertThatThrownBy(future::get).getCause().isInstanceOf(RemotingTimeoutException.class); + } + + @Test + public void testUpdateConsumerOffsetAsync_Success() throws ExecutionException, InterruptedException { + CompletableFuture remotingFuture = new CompletableFuture<>(); + remotingFuture.complete(RemotingCommand.createResponseCommand(ResponseCode.SUCCESS, "")); + doReturn(remotingFuture).when(remotingClientMock).invoke(anyString(), any(RemotingCommand.class), anyLong()); + + CompletableFuture future = mqClientAPIExt.updateConsumerOffsetAsync("brokerAddr", new UpdateConsumerOffsetRequestHeader(), 3000L); + + assertNull("Future should be completed without exception", future.get()); + } + + @Test + public void testUpdateConsumerOffsetAsync_Fail() throws InterruptedException { + + CompletableFuture remotingFuture = new CompletableFuture<>(); + remotingFuture.complete(RemotingCommand.createResponseCommand(ResponseCode.SYSTEM_ERROR, "QueueId is null, topic is testTopic")); + doReturn(remotingFuture).when(remotingClientMock).invoke(anyString(), any(RemotingCommand.class), anyLong()); + + CompletableFuture future = mqClientAPIExt.updateConsumerOffsetAsync("brokerAddr", new UpdateConsumerOffsetRequestHeader(), 3000L); + + try { + future.get(); + } catch (ExecutionException e) { + MQBrokerException customEx = (MQBrokerException) e.getCause(); + assertEquals(customEx.getResponseCode(), ResponseCode.SYSTEM_ERROR); + assertEquals(customEx.getErrorMessage(), "QueueId is null, topic is testTopic"); + } + } + + @Test + public void testRecallMessageAsync_success() { + String msgId = "msgId"; + RecallMessageRequestHeader requestHeader = new RecallMessageRequestHeader(); + requestHeader.setProducerGroup("group"); + requestHeader.setTopic("topic"); + requestHeader.setRecallHandle("handle"); + requestHeader.setBrokerName("brokerName"); + + RemotingCommand response = RemotingCommand.createResponseCommand(RecallMessageResponseHeader.class); + response.setCode(ResponseCode.SUCCESS); + RecallMessageResponseHeader responseHeader = (RecallMessageResponseHeader) response.readCustomHeader(); + responseHeader.setMsgId(msgId); + response.makeCustomHeaderToNet(); + CompletableFuture remotingFuture = new CompletableFuture<>(); + remotingFuture.complete(response); + doReturn(remotingFuture).when(remotingClientMock).invoke(anyString(), any(RemotingCommand.class), anyLong()); + + String resultId = + mqClientAPIExt.recallMessageAsync("brokerAddr", requestHeader, 3000L).join(); + Assert.assertEquals(msgId, resultId); + } + + @Test + public void testRecallMessageAsync_fail() { + RecallMessageRequestHeader requestHeader = new RecallMessageRequestHeader(); + requestHeader.setProducerGroup("group"); + requestHeader.setTopic("topic"); + requestHeader.setRecallHandle("handle"); + requestHeader.setBrokerName("brokerName"); + + CompletableFuture remotingFuture = new CompletableFuture<>(); + remotingFuture.complete(RemotingCommand.createResponseCommand(ResponseCode.SERVICE_NOT_AVAILABLE, "")); + doReturn(remotingFuture).when(remotingClientMock).invoke(anyString(), any(RemotingCommand.class), anyLong()); + + CompletionException exception = Assert.assertThrows(CompletionException.class, () -> { + mqClientAPIExt.recallMessageAsync("brokerAddr", requestHeader, 3000L).join(); + }); + Assert.assertTrue(exception.getCause() instanceof MQBrokerException); + MQBrokerException cause = (MQBrokerException) exception.getCause(); + Assert.assertEquals(ResponseCode.SERVICE_NOT_AVAILABLE, cause.getResponseCode()); + } +} \ 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 new file mode 100644 index 0000000..42ccdae --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/latency/LatencyFaultToleranceImplTest.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.client.latency; + +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; + private String brokerName = "BrokerA"; + private String anotherBrokerName = "BrokerB"; + + @Before + public void init() { + latencyFaultTolerance = new LatencyFaultToleranceImpl(null, null); + } + + @Test + public void testUpdateFaultItem() throws Exception { + 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, true); + assertThat(latencyFaultTolerance.isAvailable(brokerName)).isFalse(); + + 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, true); + assertThat(latencyFaultTolerance.isAvailable(brokerName)).isFalse(); + latencyFaultTolerance.remove(brokerName); + assertThat(latencyFaultTolerance.isAvailable(brokerName)).isTrue(); + } + + @Test + public void testPickOneAtLeast() throws Exception { + latencyFaultTolerance.updateFaultItem(brokerName, 1000, 3000, true); + 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/producer/DefaultMQProducerTest.java b/client/src/test/java/org/apache/rocketmq/client/producer/DefaultMQProducerTest.java new file mode 100644 index 0000000..33cf0df --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/producer/DefaultMQProducerTest.java @@ -0,0 +1,1016 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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; +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.latency.MQFaultStrategy; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.compression.CompressionType; +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.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; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +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 static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Fail.failBecauseExceptionWasNotThrown; +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.assertNull; +import static org.junit.Assert.assertTrue; +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.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class DefaultMQProducerTest { + @Spy + private MQClientInstance mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(new ClientConfig()); + @Mock + private MQClientAPIImpl mQClientAPIImpl; + + private DefaultMQProducer producer; + private Message message; + private Message zeroMsg; + private Message bigMessage; + private final String topic = "FooBar"; + private final String producerGroupPrefix = "FooBar_PID"; + private final long defaultTimeout = 3000L; + + @Before + 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(); + + 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)); + } + + @After + public void terminate() { + producer.shutdown(); + } + + @Test + public void testSendMessage_ZeroMessage() throws InterruptedException, RemotingException, MQBrokerException { + try { + producer.send(zeroMsg); + failBecauseExceptionWasNotThrown(MQClientException.class); + } catch (MQClientException e) { + assertThat(e).hasMessageContaining("message body length is zero"); + } + } + + @Test + public void testSendMessage_NoNameSrv() throws RemotingException, InterruptedException, MQBrokerException { + when(mQClientAPIImpl.getNameServerAddressList()).thenReturn(new ArrayList<>()); + try { + producer.send(message); + failBecauseExceptionWasNotThrown(MQClientException.class); + } catch (MQClientException e) { + assertThat(e).hasMessageContaining("No name server address"); + } + } + + @Test + public void testSendMessage_NoRoute() throws RemotingException, InterruptedException, MQBrokerException { + when(mQClientAPIImpl.getNameServerAddressList()).thenReturn(Collections.singletonList("127.0.0.1:9876")); + try { + producer.send(message); + failBecauseExceptionWasNotThrown(MQClientException.class); + } catch (MQClientException e) { + assertThat(e).hasMessageContaining("No route info of this topic"); + } + } + + @Test + public void testSendMessageSync_Success() throws RemotingException, InterruptedException, MQBrokerException, MQClientException { + 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); + } + + @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(defaultTimeout, 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(defaultTimeout, 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(defaultTimeout, 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(defaultTimeout, 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(defaultTimeout, 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(defaultTimeout, TimeUnit.MILLISECONDS); + } + + @Test + public void testSendMessageSync_SuccessWithHook() throws Throwable { + when(mQClientAPIImpl.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(createTopicRoute()); + final Throwable[] assertionErrors = new Throwable[1]; + final CountDownLatch countDownLatch = new CountDownLatch(2); + producer.getDefaultMQProducerImpl().registerSendMessageHook(new SendMessageHook() { + @Override + public String hookName() { + return "TestHook"; + } + + @Override + public void sendMessageBefore(final SendMessageContext context) { + assertionErrors[0] = assertInOtherThread(new Runnable() { + @Override + public void run() { + assertThat(context.getMessage()).isEqualTo(message); + assertThat(context.getProducer()).isEqualTo(producer); + assertThat(context.getCommunicationMode()).isEqualTo(CommunicationMode.SYNC); + assertThat(context.getSendResult()).isNull(); + } + }); + countDownLatch.countDown(); + } + + @Override + public void sendMessageAfter(final SendMessageContext context) { + assertionErrors[0] = assertInOtherThread(new Runnable() { + @Override + public void run() { + assertThat(context.getMessage()).isEqualTo(message); + assertThat(context.getProducer()).isEqualTo(producer.getDefaultMQProducerImpl()); + assertThat(context.getCommunicationMode()).isEqualTo(CommunicationMode.SYNC); + assertThat(context.getSendResult()).isNotNull(); + } + }); + countDownLatch.countDown(); + } + }); + SendResult sendResult = producer.send(message); + + assertThat(sendResult.getSendStatus()).isEqualTo(SendStatus.SEND_OK); + assertThat(sendResult.getOffsetMsgId()).isEqualTo("123"); + assertThat(sendResult.getQueueOffset()).isEqualTo(456L); + + countDownLatch.await(); + + if (assertionErrors[0] != null) { + throw assertionErrors[0]; + } + } + + @Test + public void testSetCallbackExecutor() throws MQClientException { + String producerGroupTemp = "testSetCallbackExecutor_" + System.currentTimeMillis(); + producer = new DefaultMQProducer(producerGroupTemp); + producer.setNamesrvAddr("127.0.0.1:9876"); + producer.start(); + + ExecutorService customized = Executors.newCachedThreadPool(); + producer.setCallbackExecutor(customized); + + NettyRemotingClient remotingClient = (NettyRemotingClient) producer.getDefaultMQProducerImpl() + .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(defaultTimeout, 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(defaultTimeout, 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(defaultTimeout, 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); + } + + + @Test + public void testRunningSetBackCompress() throws RemotingException, InterruptedException, MQClientException { + final CountDownLatch countDownLatch = new CountDownLatch(5); + SendCallback sendCallback = new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + countDownLatch.countDown(); + } + + @Override + public void onException(Throwable e) { + e.printStackTrace(); + countDownLatch.countDown(); + } + }; + + // on enableBackpressureForAsyncMode + producer.setEnableBackpressureForAsyncMode(true); + producer.setBackPressureForAsyncSendNum(10); + producer.setBackPressureForAsyncSendSize(50 * 1024 * 1024); + Message message = new Message(); + message.setTopic("test"); + message.setBody("hello world".getBytes()); + MessageQueue mq = new MessageQueue("test", "BrokerA", 1); + //this message is send success + for (int i = 0; i < 5; i++) { + new Thread(new Runnable() { + @Override + public void run() { + try { + producer.send(message, mq, sendCallback); + } catch (MQClientException | RemotingException | InterruptedException e) { + throw new RuntimeException(e); + } + } + }).start(); + } + producer.setBackPressureForAsyncSendNum(15); + countDownLatch.await(3000L, TimeUnit.MILLISECONDS); + assertThat(producer.defaultMQProducerImpl.getSemaphoreAsyncSendNumAvailablePermits() + countDownLatch.getCount()).isEqualTo(15); + producer.setEnableBackpressureForAsyncMode(false); + } + + 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; + } + + private Throwable assertInOtherThread(final Runnable runnable) { + final Throwable[] assertionErrors = new Throwable[1]; + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + try { + runnable.run(); + } catch (AssertionError e) { + assertionErrors[0] = e; + } + } + }); + thread.start(); + try { + thread.join(); + } catch (InterruptedException e) { + assertionErrors[0] = e; + } + return assertionErrors[0]; + } + + @Test + public void assertCreateDefaultMQProducer() { + String producerGroupTemp = producerGroupPrefix + System.currentTimeMillis(); + DefaultMQProducer producer1 = new DefaultMQProducer(producerGroupTemp); + assertNotNull(producer1); + assertEquals(producerGroupTemp, producer1.getProducerGroup()); + assertNotNull(producer1.getDefaultMQProducerImpl()); + assertEquals(0, producer1.getTotalBatchMaxBytes()); + assertEquals(0, producer1.getBatchMaxBytes()); + assertEquals(0, producer1.getBatchMaxDelayMs()); + assertNull(producer1.getTopics()); + assertFalse(producer1.isEnableTrace()); + assertTrue(UtilAll.isBlank(producer1.getTraceTopic())); + DefaultMQProducer producer2 = new DefaultMQProducer(producerGroupTemp, mock(RPCHook.class)); + assertNotNull(producer2); + assertEquals(producerGroupTemp, producer2.getProducerGroup()); + assertNotNull(producer2.getDefaultMQProducerImpl()); + assertEquals(0, producer2.getTotalBatchMaxBytes()); + assertEquals(0, producer2.getBatchMaxBytes()); + assertEquals(0, producer2.getBatchMaxDelayMs()); + assertNull(producer2.getTopics()); + assertFalse(producer2.isEnableTrace()); + assertTrue(UtilAll.isBlank(producer2.getTraceTopic())); + DefaultMQProducer producer3 = new DefaultMQProducer(producerGroupTemp, mock(RPCHook.class), Collections.singletonList("custom_topic")); + assertNotNull(producer3); + assertEquals(producerGroupTemp, producer3.getProducerGroup()); + assertNotNull(producer3.getDefaultMQProducerImpl()); + assertEquals(0, producer3.getTotalBatchMaxBytes()); + assertEquals(0, producer3.getBatchMaxBytes()); + assertEquals(0, producer3.getBatchMaxDelayMs()); + assertNotNull(producer3.getTopics()); + assertEquals(1, producer3.getTopics().size()); + assertFalse(producer3.isEnableTrace()); + assertTrue(UtilAll.isBlank(producer3.getTraceTopic())); + DefaultMQProducer producer4 = new DefaultMQProducer(producerGroupTemp, mock(RPCHook.class), true, "custom_trace_topic"); + assertNotNull(producer4); + assertEquals(producerGroupTemp, producer4.getProducerGroup()); + assertNotNull(producer4.getDefaultMQProducerImpl()); + assertEquals(0, producer4.getTotalBatchMaxBytes()); + assertEquals(0, producer4.getBatchMaxBytes()); + assertEquals(0, producer4.getBatchMaxDelayMs()); + assertNull(producer4.getTopics()); + assertTrue(producer4.isEnableTrace()); + assertEquals("custom_trace_topic", producer4.getTraceTopic()); + DefaultMQProducer producer5 = new DefaultMQProducer(producerGroupTemp, mock(RPCHook.class), Collections.singletonList("custom_topic"), true, "custom_trace_topic"); + assertNotNull(producer5); + assertEquals(producerGroupTemp, producer5.getProducerGroup()); + assertNotNull(producer5.getDefaultMQProducerImpl()); + assertEquals(0, producer5.getTotalBatchMaxBytes()); + assertEquals(0, producer5.getBatchMaxBytes()); + assertEquals(0, producer5.getBatchMaxDelayMs()); + assertNotNull(producer5.getTopics()); + assertEquals(1, producer5.getTopics().size()); + assertTrue(producer5.isEnableTrace()); + assertEquals("custom_trace_topic", producer5.getTraceTopic()); + } + + @Test + public void assertSend() throws MQBrokerException, RemotingException, InterruptedException, MQClientException, NoSuchFieldException, IllegalAccessException { + setDefaultMQProducerImpl(); + setOtherParam(); + SendResult send = producer.send(message, defaultTimeout); + assertNull(send); + Collection msgs = Collections.singletonList(message); + send = producer.send(msgs); + assertNull(send); + send = producer.send(msgs, defaultTimeout); + assertNull(send); + } + + @Test + public void assertSendOneway() throws RemotingException, InterruptedException, MQClientException, NoSuchFieldException, IllegalAccessException { + setDefaultMQProducerImpl(); + producer.sendOneway(message); + MessageQueue mq = mock(MessageQueue.class); + producer.sendOneway(message, mq); + MessageQueueSelector selector = mock(MessageQueueSelector.class); + producer.sendOneway(message, selector, 1); + } + + @Test + public void assertSendByQueue() throws MQBrokerException, RemotingException, InterruptedException, MQClientException, NoSuchFieldException, IllegalAccessException { + setDefaultMQProducerImpl(); + MessageQueue mq = mock(MessageQueue.class); + SendResult send = producer.send(message, mq); + assertNull(send); + send = producer.send(message, mq, defaultTimeout); + assertNull(send); + Collection msgs = Collections.singletonList(message); + send = producer.send(msgs, mq); + assertNull(send); + send = producer.send(msgs, mq, defaultTimeout); + assertNull(send); + } + + @Test + public void assertSendByQueueSelector() throws MQBrokerException, RemotingException, InterruptedException, MQClientException, NoSuchFieldException, IllegalAccessException { + setDefaultMQProducerImpl(); + MessageQueueSelector selector = mock(MessageQueueSelector.class); + SendResult send = producer.send(message, selector, 1); + assertNull(send); + send = producer.send(message, selector, 1, defaultTimeout); + assertNull(send); + } + + @Test + public void assertRequest() throws MQBrokerException, RemotingException, InterruptedException, MQClientException, NoSuchFieldException, IllegalAccessException, RequestTimeoutException { + setDefaultMQProducerImpl(); + MessageQueueSelector selector = mock(MessageQueueSelector.class); + Message replyNsg = producer.request(message, selector, 1, defaultTimeout); + assertNull(replyNsg); + RequestCallback requestCallback = mock(RequestCallback.class); + producer.request(message, selector, 1, requestCallback, defaultTimeout); + MessageQueue mq = mock(MessageQueue.class); + producer.request(message, mq, defaultTimeout); + producer.request(message, mq, requestCallback, defaultTimeout); + } + + @Test(expected = RuntimeException.class) + public void assertSendMessageInTransaction() throws MQClientException { + TransactionSendResult result = producer.sendMessageInTransaction(message, 1); + assertNull(result); + } + + @Test + public void assertSearchOffset() throws MQClientException, NoSuchFieldException, IllegalAccessException { + setDefaultMQProducerImpl(); + MessageQueue mq = mock(MessageQueue.class); + long result = producer.searchOffset(mq, System.currentTimeMillis()); + assertEquals(0L, result); + } + + @Test + public void assertBatchMaxDelayMs() throws NoSuchFieldException, IllegalAccessException { + setProduceAccumulator(true); + assertEquals(0, producer.getBatchMaxDelayMs()); + setProduceAccumulator(false); + assertEquals(10, producer.getBatchMaxDelayMs()); + producer.batchMaxDelayMs(1000); + assertEquals(1000, producer.getBatchMaxDelayMs()); + } + + @Test + public void assertBatchMaxBytes() throws NoSuchFieldException, IllegalAccessException { + setProduceAccumulator(true); + assertEquals(0L, producer.getBatchMaxBytes()); + setProduceAccumulator(false); + assertEquals(32 * 1024L, producer.getBatchMaxBytes()); + producer.batchMaxBytes(64 * 1024L); + assertEquals(64 * 1024L, producer.getBatchMaxBytes()); + } + + @Test + public void assertTotalBatchMaxBytes() throws NoSuchFieldException, IllegalAccessException { + setProduceAccumulator(true); + assertEquals(0L, producer.getTotalBatchMaxBytes()); + } + + @Test + public void assertProduceAccumulatorStart() throws NoSuchFieldException, IllegalAccessException, MQClientException { + String producerGroupTemp = producerGroupPrefix + System.nanoTime(); + DefaultMQProducer producer = new DefaultMQProducer(producerGroupTemp); + assertEquals(0, producer.getTotalBatchMaxBytes()); + assertEquals(0, producer.getBatchMaxBytes()); + assertEquals(0, producer.getBatchMaxDelayMs()); + assertNull(getField(producer, "produceAccumulator", ProduceAccumulator.class)); + producer.start(); + assertTrue(producer.getTotalBatchMaxBytes() > 0); + assertTrue(producer.getBatchMaxBytes() > 0); + assertTrue(producer.getBatchMaxDelayMs() > 0); + assertNotNull(getField(producer, "produceAccumulator", ProduceAccumulator.class)); + } + + @Test + public void assertProduceAccumulatorBeforeStartSet() throws NoSuchFieldException, IllegalAccessException, MQClientException { + String producerGroupTemp = producerGroupPrefix + System.nanoTime(); + DefaultMQProducer producer = new DefaultMQProducer(producerGroupTemp); + producer.totalBatchMaxBytes(64 * 1024 * 100); + producer.batchMaxBytes(64 * 1024); + producer.batchMaxDelayMs(10); + + producer.start(); + assertEquals(64 * 1024, producer.getBatchMaxBytes()); + assertEquals(10, producer.getBatchMaxDelayMs()); + assertNotNull(getField(producer, "produceAccumulator", ProduceAccumulator.class)); + } + + @Test + public void assertProduceAccumulatorAfterStartSet() throws NoSuchFieldException, IllegalAccessException, MQClientException { + String producerGroupTemp = producerGroupPrefix + System.nanoTime(); + DefaultMQProducer producer = new DefaultMQProducer(producerGroupTemp); + producer.start(); + + assertNotNull(getField(producer, "produceAccumulator", ProduceAccumulator.class)); + + producer.totalBatchMaxBytes(64 * 1024 * 100); + producer.batchMaxBytes(64 * 1024); + producer.batchMaxDelayMs(10); + + assertEquals(64 * 1024, producer.getBatchMaxBytes()); + assertEquals(10, producer.getBatchMaxDelayMs()); + } + + @Test + public void assertProduceAccumulatorUnit() throws NoSuchFieldException, IllegalAccessException, MQClientException { + String producerGroupTemp = producerGroupPrefix + System.nanoTime(); + DefaultMQProducer producer1 = new DefaultMQProducer(producerGroupTemp); + producer1.setUnitName("unit1"); + DefaultMQProducer producer2 = new DefaultMQProducer(producerGroupTemp); + producer2.setUnitName("unit2"); + + producer1.start(); + producer2.start(); + + ProduceAccumulator producer1Accumulator = getField(producer1, "produceAccumulator", ProduceAccumulator.class); + ProduceAccumulator producer2Accumulator = getField(producer2, "produceAccumulator", ProduceAccumulator.class); + + assertNotNull(producer1Accumulator); + assertNotNull(producer2Accumulator); + + assertNotEquals(producer1Accumulator, producer2Accumulator); + } + + @Test + public void assertProduceAccumulator() throws NoSuchFieldException, IllegalAccessException, MQClientException { + String producerGroupTemp1 = producerGroupPrefix + System.nanoTime(); + DefaultMQProducer producer1 = new DefaultMQProducer(producerGroupTemp1); + producer1.setInstanceName("instanceName1"); + String producerGroupTemp2 = producerGroupPrefix + System.nanoTime(); + DefaultMQProducer producer2 = new DefaultMQProducer(producerGroupTemp2); + producer2.setInstanceName("instanceName2"); + + producer1.start(); + producer2.start(); + + ProduceAccumulator producer1Accumulator = getField(producer1, "produceAccumulator", ProduceAccumulator.class); + ProduceAccumulator producer2Accumulator = getField(producer2, "produceAccumulator", ProduceAccumulator.class); + + assertNotNull(producer1Accumulator); + assertNotNull(producer2Accumulator); + + assertNotEquals(producer1Accumulator, producer2Accumulator); + } + + @Test + public void assertProduceAccumulatorInstanceEqual() throws NoSuchFieldException, IllegalAccessException, MQClientException { + String producerGroupTemp1 = producerGroupPrefix + System.nanoTime(); + DefaultMQProducer producer1 = new DefaultMQProducer(producerGroupTemp1); + producer1.setInstanceName("equalInstance"); + String producerGroupTemp2 = producerGroupPrefix + System.nanoTime(); + DefaultMQProducer producer2 = new DefaultMQProducer(producerGroupTemp2); + producer2.setInstanceName("equalInstance"); + + producer1.start(); + producer2.start(); + + ProduceAccumulator producer1Accumulator = getField(producer1, "produceAccumulator", ProduceAccumulator.class); + ProduceAccumulator producer2Accumulator = getField(producer2, "produceAccumulator", ProduceAccumulator.class); + + assertNotNull(producer1Accumulator); + assertNotNull(producer2Accumulator); + + assertEquals(producer1Accumulator, producer2Accumulator); + } + + @Test + public void assertProduceAccumulatorInstanceAndUnitNameEqual() throws NoSuchFieldException, IllegalAccessException, MQClientException { + String producerGroupTemp1 = producerGroupPrefix + System.nanoTime(); + DefaultMQProducer producer1 = new DefaultMQProducer(producerGroupTemp1); + producer1.setInstanceName("equalInstance"); + producer1.setUnitName("equalUnitName"); + String producerGroupTemp2 = producerGroupPrefix + System.nanoTime(); + DefaultMQProducer producer2 = new DefaultMQProducer(producerGroupTemp2); + producer2.setInstanceName("equalInstance"); + producer2.setUnitName("equalUnitName"); + + producer1.start(); + producer2.start(); + + ProduceAccumulator producer1Accumulator = getField(producer1, "produceAccumulator", ProduceAccumulator.class); + ProduceAccumulator producer2Accumulator = getField(producer2, "produceAccumulator", ProduceAccumulator.class); + + assertNotNull(producer1Accumulator); + assertNotNull(producer2Accumulator); + + assertEquals(producer1Accumulator, producer2Accumulator); + } + + @Test + public void assertGetRetryResponseCodes() { + assertNotNull(producer.getRetryResponseCodes()); + assertEquals(8, producer.getRetryResponseCodes().size()); + } + + @Test + public void assertIsSendLatencyFaultEnable() { + assertFalse(producer.isSendLatencyFaultEnable()); + } + + @Test + public void assertGetLatencyMax() { + assertNotNull(producer.getLatencyMax()); + } + + @Test + public void assertGetNotAvailableDuration() { + assertNotNull(producer.getNotAvailableDuration()); + } + + @Test + public void assertIsRetryAnotherBrokerWhenNotStoreOK() { + assertFalse(producer.isRetryAnotherBrokerWhenNotStoreOK()); + } + + private void setOtherParam() { + producer.setCreateTopicKey("createTopicKey"); + producer.setRetryAnotherBrokerWhenNotStoreOK(false); + producer.setDefaultTopicQueueNums(6); + producer.setRetryTimesWhenSendFailed(1); + producer.setSendMessageWithVIPChannel(false); + producer.setNotAvailableDuration(new long[1]); + producer.setLatencyMax(new long[1]); + producer.setSendLatencyFaultEnable(false); + producer.setRetryTimesWhenSendAsyncFailed(1); + producer.setTopics(Collections.singletonList(topic)); + producer.setStartDetectorEnable(false); + producer.setCompressLevel(5); + producer.setCompressType(CompressionType.LZ4); + producer.addRetryResponseCode(0); + ExecutorService executorService = mock(ExecutorService.class); + producer.setAsyncSenderExecutor(executorService); + } + + private void setProduceAccumulator(final boolean isDefault) throws NoSuchFieldException, IllegalAccessException { + ProduceAccumulator accumulator = null; + if (!isDefault) { + accumulator = new ProduceAccumulator("instanceName"); + } + setField(producer, "produceAccumulator", accumulator); + } + + private void setDefaultMQProducerImpl() throws NoSuchFieldException, IllegalAccessException { + DefaultMQProducerImpl producerImpl = mock(DefaultMQProducerImpl.class); + setField(producer, "defaultMQProducerImpl", producerImpl); + when(producerImpl.getMqFaultStrategy()).thenReturn(mock(MQFaultStrategy.class)); + } + + private void setField(final Object target, final String fieldName, final Object newValue) throws NoSuchFieldException, IllegalAccessException { + Class clazz = target.getClass(); + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, newValue); + } + + private T getField(final Object target, final String fieldName, final Class fieldClassType) throws NoSuchFieldException, IllegalAccessException { + Class targetClazz = target.getClass(); + Field field = targetClazz.getDeclaredField(fieldName); + field.setAccessible(true); + return fieldClassType.cast(field.get(target)); + } +} 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 0000000..8e76238 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/producer/ProduceAccumulatorTest.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.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.batchMaxDelayMs(3000); + 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(5000L, 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 0000000..68615f3 --- /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/DefaultMQProducerImplTest.java b/client/src/test/java/org/apache/rocketmq/client/producer/selector/DefaultMQProducerImplTest.java new file mode 100644 index 0000000..77a83af --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/producer/selector/DefaultMQProducerImplTest.java @@ -0,0 +1,385 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.CheckForbiddenContext; +import org.apache.rocketmq.client.hook.CheckForbiddenHook; +import org.apache.rocketmq.client.impl.MQAdminImpl; +import org.apache.rocketmq.client.impl.MQClientAPIImpl; +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.MessageQueueSelector; +import org.apache.rocketmq.client.producer.RequestCallback; +import org.apache.rocketmq.client.producer.SendCallback; +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.common.MixAll; +import org.apache.rocketmq.common.ServiceState; +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.MessageQueue; +import org.apache.rocketmq.common.producer.RecallMessageHandle; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.header.CheckTransactionStateRequestHeader; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutorService; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertThrows; +import static org.mockito.AdditionalMatchers.or; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class DefaultMQProducerImplTest { + + @Mock + private Message message; + + @Mock + private MessageQueue messageQueue; + + @Mock + private MessageQueueSelector queueSelector; + + @Mock + private RequestCallback requestCallback; + + @Mock + private MQClientInstance mQClientFactory; + + @Mock + private MQClientAPIImpl mQClientAPIImpl; + + private DefaultMQProducerImpl defaultMQProducerImpl; + + private final long defaultTimeout = 30000L; + + private final String defaultBrokerName = "broker-0"; + + private final String defaultBrokerAddr = "127.0.0.1:10911"; + + private final String defaultTopic = "testTopic"; + + @Before + public void init() throws Exception { + when(mQClientFactory.getTopicRouteTable()).thenReturn(mock(ConcurrentMap.class)); + when(mQClientFactory.getClientId()).thenReturn("client-id"); + when(mQClientFactory.getMQAdminImpl()).thenReturn(mock(MQAdminImpl.class)); + ClientConfig clientConfig = mock(ClientConfig.class); + when(messageQueue.getTopic()).thenReturn(defaultTopic); + when(clientConfig.queueWithNamespace(any())).thenReturn(messageQueue); + when(mQClientFactory.getClientConfig()).thenReturn(clientConfig); + when(mQClientFactory.getTopicRouteTable()).thenReturn(mock(ConcurrentMap.class)); + when(mQClientFactory.getMQClientAPIImpl()).thenReturn(mQClientAPIImpl); + when(mQClientFactory.findBrokerAddressInPublish(or(isNull(), anyString()))).thenReturn(defaultBrokerAddr); + when(message.getTopic()).thenReturn(defaultTopic); + when(message.getProperty(MessageConst.PROPERTY_CORRELATION_ID)).thenReturn("correlation-id"); + when(message.getBody()).thenReturn(new byte[1]); + TransactionMQProducer producer = new TransactionMQProducer("test-producer-group"); + producer.setTransactionListener(mock(TransactionListener.class)); + producer.setTopics(Collections.singletonList(defaultTopic)); + defaultMQProducerImpl = new DefaultMQProducerImpl(producer); + setMQClientFactory(); + setCheckExecutor(); + setCheckForbiddenHookList(); + setTopicPublishInfoTable(); + defaultMQProducerImpl.setServiceState(ServiceState.RUNNING); + } + + @Test + public void testRequest() throws Exception { + defaultMQProducerImpl.request(message, messageQueue, requestCallback, defaultTimeout); + defaultMQProducerImpl.request(message, queueSelector, 1, requestCallback, defaultTimeout); + } + + @Test(expected = MQClientException.class) + public void testRequestMQClientExceptionByVoid() throws Exception { + defaultMQProducerImpl.request(message, requestCallback, defaultTimeout); + } + + @Test + public void testCheckTransactionState() { + defaultMQProducerImpl.checkTransactionState(defaultBrokerAddr, mock(MessageExt.class), mock(CheckTransactionStateRequestHeader.class)); + } + + @Test + public void testCreateTopic() throws MQClientException { + defaultMQProducerImpl.createTopic("key", defaultTopic, 0); + } + + @Test + public void testExecuteCheckForbiddenHook() throws MQClientException { + defaultMQProducerImpl.executeCheckForbiddenHook(mock(CheckForbiddenContext.class)); + } + + @Test(expected = MQClientException.class) + public void testSendOneway() throws MQClientException, InterruptedException, RemotingException { + defaultMQProducerImpl.sendOneway(message); + } + + @Test + public void testSendOnewayByQueueSelector() throws MQClientException, InterruptedException, RemotingException { + defaultMQProducerImpl.sendOneway(message, mock(MessageQueueSelector.class), 1); + } + + @Test + public void testSendOnewayByQueue() throws MQClientException, InterruptedException, RemotingException { + defaultMQProducerImpl.sendOneway(message, messageQueue); + } + + @Test(expected = MQClientException.class) + public void testSend() throws RemotingException, InterruptedException, MQClientException, MQBrokerException { + assertNull(defaultMQProducerImpl.send(message)); + } + + @Test + public void assertSendByQueue() throws InterruptedException, MQBrokerException, RemotingException, MQClientException { + SendResult actual = defaultMQProducerImpl.send(message, messageQueue); + assertNull(actual); + actual = defaultMQProducerImpl.send(message, messageQueue, defaultTimeout); + assertNull(actual); + } + + @Test + public void assertSendByQueueSelector() throws InterruptedException, MQBrokerException, RemotingException, MQClientException { + SendCallback sendCallback = mock(SendCallback.class); + defaultMQProducerImpl.send(message, queueSelector, 1, sendCallback); + SendResult actual = defaultMQProducerImpl.send(message, queueSelector, 1); + assertNull(actual); + actual = defaultMQProducerImpl.send(message, queueSelector, 1, defaultTimeout); + assertNull(actual); + } + + @Test(expected = MQClientException.class) + public void assertMQClientException() throws Exception { + assertNull(defaultMQProducerImpl.request(message, defaultTimeout)); + } + + @Test(expected = RequestTimeoutException.class) + public void assertRequestRequestTimeoutByQueueSelector() throws Exception { + assertNull(defaultMQProducerImpl.request(message, queueSelector, 1, 3000L)); + } + + @Test(expected = Exception.class) + public void assertRequestTimeoutExceptionByQueue() throws Exception { + assertNull(defaultMQProducerImpl.request(message, messageQueue, 3000L)); + } + + @Test + public void testRegisterCheckForbiddenHook() { + CheckForbiddenHook checkForbiddenHook = mock(CheckForbiddenHook.class); + defaultMQProducerImpl.registerCheckForbiddenHook(checkForbiddenHook); + } + + @Test + public void testInitTopicRoute() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Class clazz = defaultMQProducerImpl.getClass(); + Method method = clazz.getDeclaredMethod("initTopicRoute"); + method.setAccessible(true); + method.invoke(defaultMQProducerImpl); + } + + @Test + public void assertFetchPublishMessageQueues() throws MQClientException { + List actual = defaultMQProducerImpl.fetchPublishMessageQueues(defaultTopic); + assertNotNull(actual); + assertEquals(0, actual.size()); + } + + @Test + public void assertSearchOffset() throws MQClientException { + assertEquals(0, defaultMQProducerImpl.searchOffset(messageQueue, System.currentTimeMillis())); + } + + @Test + public void assertMaxOffset() throws MQClientException { + assertEquals(0, defaultMQProducerImpl.maxOffset(messageQueue)); + } + + @Test + public void assertMinOffset() throws MQClientException { + assertEquals(0, defaultMQProducerImpl.minOffset(messageQueue)); + } + + @Test + public void assertEarliestMsgStoreTime() throws MQClientException { + assertEquals(0, defaultMQProducerImpl.earliestMsgStoreTime(messageQueue)); + } + + @Test + public void assertViewMessage() throws MQClientException, MQBrokerException, RemotingException, InterruptedException { + assertNull(defaultMQProducerImpl.viewMessage(defaultTopic, "msgId")); + } + + @Test + public void assertQueryMessage() throws MQClientException, InterruptedException { + assertNull(defaultMQProducerImpl.queryMessage(defaultTopic, "key", 1, 0L, 10L)); + } + + @Test + public void assertQueryMessageByUniqKey() throws MQClientException, InterruptedException { + assertNull(defaultMQProducerImpl.queryMessageByUniqKey(defaultTopic, "key")); + } + + @Test + public void assertSetAsyncSenderExecutor() { + ExecutorService asyncSenderExecutor = mock(ExecutorService.class); + defaultMQProducerImpl.setAsyncSenderExecutor(asyncSenderExecutor); + assertEquals(asyncSenderExecutor, defaultMQProducerImpl.getAsyncSenderExecutor()); + } + + @Test + public void assertServiceState() { + ServiceState serviceState = defaultMQProducerImpl.getServiceState(); + assertNotNull(serviceState); + assertEquals(ServiceState.RUNNING, serviceState); + defaultMQProducerImpl.setServiceState(ServiceState.SHUTDOWN_ALREADY); + serviceState = defaultMQProducerImpl.getServiceState(); + assertNotNull(serviceState); + assertEquals(ServiceState.SHUTDOWN_ALREADY, serviceState); + } + + @Test + public void assertGetNotAvailableDuration() { + long[] notAvailableDuration = defaultMQProducerImpl.getNotAvailableDuration(); + assertNotNull(notAvailableDuration); + defaultMQProducerImpl.setNotAvailableDuration(new long[1]); + notAvailableDuration = defaultMQProducerImpl.getNotAvailableDuration(); + assertNotNull(notAvailableDuration); + assertEquals(1, notAvailableDuration.length); + } + + @Test + public void assertGetLatencyMax() { + long[] actual = defaultMQProducerImpl.getLatencyMax(); + assertNotNull(actual); + defaultMQProducerImpl.setLatencyMax(new long[1]); + actual = defaultMQProducerImpl.getLatencyMax(); + assertNotNull(actual); + assertEquals(1, actual.length); + } + + @Test + public void assertIsSendLatencyFaultEnable() { + boolean actual = defaultMQProducerImpl.isSendLatencyFaultEnable(); + assertFalse(actual); + defaultMQProducerImpl.setSendLatencyFaultEnable(true); + actual = defaultMQProducerImpl.isSendLatencyFaultEnable(); + assertTrue(actual); + } + + @Test + public void assertGetMqFaultStrategy() { + assertNotNull(defaultMQProducerImpl.getMqFaultStrategy()); + } + + @Test + public void assertCheckListener() { + assertNull(defaultMQProducerImpl.checkListener()); + } + + @Test + public void testRecallMessage_invalid() { + assertThrows(MQClientException.class, () -> { + defaultMQProducerImpl.recallMessage(MixAll.REPLY_TOPIC_POSTFIX + defaultTopic, "handle"); + }); + assertThrows(MQClientException.class, () -> { + defaultMQProducerImpl.recallMessage(MixAll.DLQ_GROUP_TOPIC_PREFIX + defaultTopic, "handle"); + }); + assertThrows(MQClientException.class, () -> { + defaultMQProducerImpl.recallMessage(defaultTopic, "handle"); + }); + } + + @Test + public void testRecallMessage_addressNotFound() { + String handle = RecallMessageHandle.HandleV1.buildHandle(defaultTopic, defaultBrokerName, "1", "id"); + when(mQClientFactory.findBrokerAddressInPublish(defaultBrokerName)).thenReturn(null); + MQClientException e = assertThrows(MQClientException.class, () -> { + defaultMQProducerImpl.recallMessage(defaultTopic, handle); + }); + assertEquals("The broker service address not found", e.getErrorMessage()); + } + + @Test + public void testRecallMessage_success() + throws RemotingException, MQClientException, MQBrokerException, InterruptedException { + String handle = RecallMessageHandle.HandleV1.buildHandle(defaultTopic, defaultBrokerName, "1", "id"); + when(mQClientFactory.findBrokerAddressInPublish(defaultBrokerName)).thenReturn(defaultBrokerAddr); + when(mQClientAPIImpl.recallMessage(any(), any(), anyLong())).thenReturn("id"); + String result = defaultMQProducerImpl.recallMessage(defaultTopic, handle); + assertEquals("id", result); + } + + private void setMQClientFactory() throws IllegalAccessException, NoSuchFieldException { + setField(defaultMQProducerImpl, "mQClientFactory", mQClientFactory); + } + + private void setTopicPublishInfoTable() throws IllegalAccessException, NoSuchFieldException { + ConcurrentMap topicPublishInfoTable = new ConcurrentHashMap<>(); + TopicPublishInfo topicPublishInfo = mock(TopicPublishInfo.class); + when(topicPublishInfo.ok()).thenReturn(true); + topicPublishInfoTable.put(defaultTopic, topicPublishInfo); + setField(defaultMQProducerImpl, "topicPublishInfoTable", topicPublishInfoTable); + } + + private void setCheckExecutor() throws NoSuchFieldException, IllegalAccessException { + setField(defaultMQProducerImpl, "checkExecutor", mock(ExecutorService.class)); + } + + private void setCheckForbiddenHookList() throws NoSuchFieldException, IllegalAccessException { + ArrayList checkForbiddenHookList = new ArrayList<>(); + checkForbiddenHookList.add(mock(CheckForbiddenHook.class)); + setField(defaultMQProducerImpl, "checkForbiddenHookList", checkForbiddenHookList); + } + + private void setField(final Object target, final String fieldName, final Object newValue) throws NoSuchFieldException, IllegalAccessException { + Class clazz = target.getClass(); + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, newValue); + } +} 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 new file mode 100644 index 0000000..8d5a24b --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/producer/selector/SelectMessageQueueByHashTest.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.producer.selector; + +import java.util.ArrayList; +import java.util.List; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageQueue; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SelectMessageQueueByHashTest { + + private String topic = "FooBar"; + + @Test + public void testSelect() throws Exception { + SelectMessageQueueByHash selector = new SelectMessageQueueByHash(); + + Message message = new Message(topic, new byte[] {}); + + List messageQueues = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + MessageQueue messageQueue = new MessageQueue(topic, "DefaultBroker", i); + messageQueues.add(messageQueue); + } + + String orderId = "123"; + 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/SelectMessageQueueByRandomTest.java b/client/src/test/java/org/apache/rocketmq/client/producer/selector/SelectMessageQueueByRandomTest.java new file mode 100644 index 0000000..9443c3f --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/producer/selector/SelectMessageQueueByRandomTest.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.client.producer.selector; + +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageQueue; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; + +public class SelectMessageQueueByRandomTest { + + private final SelectMessageQueueByRandom selector = new SelectMessageQueueByRandom(); + + private final String defaultBroker = "defaultBroker"; + + private final String defaultTopic = "defaultTopic"; + + @Test + public void testSelectRandomMessageQueue() { + List messageQueues = createMessageQueues(10); + Message message = new Message(defaultTopic, "tag", "key", "body".getBytes()); + MessageQueue selectedQueue = selector.select(messageQueues, message, null); + assertNotNull(selectedQueue); + assertEquals(messageQueues.size(), 10); + assertEquals(defaultTopic, selectedQueue.getTopic()); + assertEquals(defaultBroker, selectedQueue.getBrokerName()); + } + + @Test + public void testSelectEmptyMessageQueue() { + List emptyQueues = new ArrayList<>(); + Message message = new Message(defaultTopic, "tag", "key", "body".getBytes()); + assertThrows(IllegalArgumentException.class, () -> selector.select(emptyQueues, message, null)); + } + + @Test + public void testSelectSingleMessageQueue() { + List singleQueueList = createMessageQueues(1); + Message message = new Message(defaultTopic, "tag", "key", "body".getBytes()); + MessageQueue selectedQueue = selector.select(singleQueueList, message, null); + assertNotNull(selectedQueue); + assertEquals(defaultTopic, selectedQueue.getTopic()); + assertEquals(defaultBroker, selectedQueue.getBrokerName()); + assertEquals(singleQueueList.get(0).getQueueId(), selectedQueue.getQueueId()); + } + + private List createMessageQueues(final int count) { + List result = new ArrayList<>(); + for (int i = 0; i < count; i++) { + result.add(new MessageQueue(defaultTopic, defaultBroker, i)); + } + return result; + } +} 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 0000000..df4dd87 --- /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 0000000..385c2ce --- /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 0000000..028445e --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/trace/DefaultMQConsumerWithOpenTracingTest.java @@ -0,0 +1,241 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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); + // disable trace to let mock trace work + pushConsumer.setEnableTrace(false); + + 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 0000000..fc63cce --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/trace/DefaultMQConsumerWithTraceTest.java @@ -0,0 +1,337 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.setUseTLS(true); + pushConsumer.setPullInterval(60 * 1000); + + 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(); + + asyncTraceDispatcher = (AsyncTraceDispatcher) pushConsumer.getTraceDispatcher(); + traceProducer = asyncTraceDispatcher.getTraceProducer(); + + 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() { + 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 0000000..e0573bd --- /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 0000000..c0eb856 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/trace/DefaultMQProducerWithOpenTracingTest.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 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'}); + // disable trace to let mock trace work + producer.setEnableTrace(false); + + 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_STORE_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 0000000..ed680d8 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/trace/DefaultMQProducerWithTraceTest.java @@ -0,0 +1,222 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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'}); + producer.setTraceTopic(customerTraceTopic); + producer.setUseTLS(true); + + producer.start(); + + asyncTraceDispatcher = (AsyncTraceDispatcher) producer.getTraceDispatcher(); + traceProducer = asyncTraceDispatcher.getTraceProducer(); + + 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() { + 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 0000000..26b7bda --- /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 0000000..0397db2 --- /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 0000000..5d4b81d --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/trace/TransactionMQProducerWithOpenTracingTest.java @@ -0,0 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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); + // disable trace to let mock trace work + producer.setEnableTrace(false); + + 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 0000000..9f60361 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/trace/TransactionMQProducerWithTraceTest.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.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'}); + + producer.start(); + + asyncTraceDispatcher = (AsyncTraceDispatcher) producer.getTraceDispatcher(); + traceProducer = asyncTraceDispatcher.getTraceProducer(); + + 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 0000000..803e596 --- /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/acl_hook/plain_acl.yml b/client/src/test/resources/acl_hook/plain_acl.yml new file mode 100644 index 0000000..66bf576 --- /dev/null +++ b/client/src/test/resources/acl_hook/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 + +- accessKey: rocketmq2 + secretKey: 12345678 + whiteRemoteAddress: 192.168.1.* + admin: true diff --git a/client/src/test/resources/conf/plain_acl_incomplete.yml b/client/src/test/resources/conf/plain_acl_incomplete.yml new file mode 100644 index 0000000..9ac39c8 --- /dev/null +++ b/client/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/client/src/test/resources/org/powermock/extensions/configuration.properties b/client/src/test/resources/org/powermock/extensions/configuration.properties new file mode 100644 index 0000000..6389eff --- /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 0000000..8695d52 --- /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 0000000..10c5d19 --- /dev/null +++ b/common/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 = "common", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "@maven//:com_alibaba_fastjson", + "@maven//:com_alibaba_fastjson2_fastjson2", + "@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//:io_grpc_grpc_api", + "@maven//:io_grpc_grpc_context", + "@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//:com_alibaba_fastjson2_fastjson2", + "@maven//:commons_codec_commons_codec", + "@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_grpc_grpc_api", + "@maven//:io_grpc_grpc_context", + "@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 new file mode 100644 index 0000000..4d7dde2 --- /dev/null +++ b/common/pom.xml @@ -0,0 +1,120 @@ + + + + + org.apache.rocketmq + rocketmq-all + 5.3.4-SNAPSHOT + + + 4.0.0 + jar + rocketmq-common + rocketmq-common ${project.version} + + + ${basedir}/.. + + + + + com.alibaba + fastjson + + + com.alibaba.fastjson2 + fastjson2 + + + 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 0000000..562fdaa --- /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 0000000..34aabc5 --- /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/BoundaryType.java b/common/src/main/java/org/apache/rocketmq/common/BoundaryType.java new file mode 100644 index 0000000..03a0171 --- /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 new file mode 100644 index 0000000..b607985 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/BrokerConfig.java @@ -0,0 +1,2063 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.annotation.ImportantField; +import org.apache.rocketmq.common.config.ConfigManagerVersion; +import org.apache.rocketmq.common.constant.PermName; +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.util.concurrent.TimeUnit; + +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 int listenPort = 6888; + + @ImportantField + private String brokerIP1 = NetworkUtil.getLocalAddress(); + private String brokerIP2 = NetworkUtil.getLocalAddress(); + + @ImportantField + private boolean recoverConcurrently = false; + + private int brokerPermission = PermName.PERM_READ | PermName.PERM_WRITE; + private int defaultTopicQueueNums = 8; + @ImportantField + private boolean autoCreateTopicEnable = true; + + private boolean clusterTopicEnable = true; + + private boolean brokerTopicEnable = true; + @ImportantField + 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. + */ + 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 = 16; + 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; + + private int flushConsumerOffsetHistoryInterval = 1000 * 60; + + @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 heartbeatThreadPoolQueueCapacity = 50000; + private int endTransactionPoolQueueCapacity = 100000; + private int adminBrokerThreadPoolQueueCapacity = 10000; + private int loadBalanceThreadPoolQueueCapacity = 100000; + + private boolean longPollingEnable = true; + + private long shortPollingTimeMills = 1000; + + private boolean notifyConsumerIdsChangedEnable = true; + + private boolean highSpeedMode = false; + + private int commercialBaseCount = 1; + + private int commercialSizePerMsg = 4 * 1024; + + private boolean accountStatsEnable = true; + private boolean accountStatsPrintZeroValues = true; + + private int maxStatsIdleTimeInMinutes = -1; + + private boolean transferMsgByHeap = true; + + private String regionId = MixAll.DEFAULT_TRACE_REGION_ID; + private int registerBrokerTimeoutMills = 24000; + + private int sendHeartbeatTimeoutMillis = 1000; + + private boolean slaveReadEnable = false; + + private boolean disableConsumeIfConsumerReadSlowly = false; + private long consumerFallbehindThreshold = 1024L * 1024 * 1024 * 16; + + 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 waitTimeMillsInAdminBrokerQueue = 5 * 1000; + private long startAcceptSendRequestTimeStamp = 0L; + + private boolean traceOn = true; + + // Switch of filter bit map calculation. + // If switch on: + // 1. Calculate filter bit map when construct queue. + // 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; + + // Error rate of bloom filter, 1~100. + private int maxErrorRateOfBloomFilter = 20; + + //how long to clean filter data after dead.Default: 24h + private long filterDataCleanTimeSpan = 24 * 3600 * 1000; + + // whether do filter when retry. + 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; + + /** + * This configurable item defines interval of update name server address. Default: 120 * 1000 milliseconds + */ + private int updateNameServerAddrPeriod = 1000 * 120; + + /** + * 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; + // set the interval to the maxFilterMessageSize in MessageStoreConfig divided by the cq unit size + private long popLongPollingForceNotifyInterval = 800; + private boolean enableNotifyBeforePopCalculateLag = true; + 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 int popFromRetryProbability = 20; + private boolean popConsumerFSServiceInit = true; + private boolean popConsumerKVServiceLog = false; + private boolean popConsumerKVServiceInit = false; + private boolean popConsumerKVServiceEnable = false; + private int popReviveMaxReturnSizePerRead = 16 * 1024; + private int popReviveMaxAttemptTimes = 16; + + 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 = 10 * 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 = true; + + /** + * 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 boolean enableFastChannelEventProcess = false; + private boolean printChannelGroups = false; + private int printChannelGroupsMinNum = 5; + + 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"; + + // if false, will still rewrite ck after max times 17 + private boolean skipWhenCKRePutReachMaxTimes = false; + + private boolean appendAckAsync = false; + + private boolean appendCkAsync = false; + + private boolean clearRetryTopicWhenDeleteTopic = true; + + private boolean enableLmqStats = false; + + /** + * V2 is recommended in cases where LMQ feature is extensively used. + */ + private String configManagerVersion = ConfigManagerVersion.V1.getVersion(); + + private boolean allowRecallWhenBrokerNotWriteable = true; + + private boolean recallMessageEnable = false; + + private boolean enableRegisterProducer = true; + + private boolean enableCreateSysGroup = true; + + 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 int getPopFromRetryProbability() { + return popFromRetryProbability; + } + + public void setPopFromRetryProbability(int popFromRetryProbability) { + this.popFromRetryProbability = popFromRetryProbability; + } + + public boolean isPopConsumerFSServiceInit() { + return popConsumerFSServiceInit; + } + + public void setPopConsumerFSServiceInit(boolean popConsumerFSServiceInit) { + this.popConsumerFSServiceInit = popConsumerFSServiceInit; + } + + public boolean isPopConsumerKVServiceLog() { + return popConsumerKVServiceLog; + } + + public void setPopConsumerKVServiceLog(boolean popConsumerKVServiceLog) { + this.popConsumerKVServiceLog = popConsumerKVServiceLog; + } + + public boolean isPopConsumerKVServiceInit() { + return popConsumerKVServiceInit; + } + + public void setPopConsumerKVServiceInit(boolean popConsumerKVServiceInit) { + this.popConsumerKVServiceInit = popConsumerKVServiceInit; + } + + public boolean isPopConsumerKVServiceEnable() { + return popConsumerKVServiceEnable; + } + + public void setPopConsumerKVServiceEnable(boolean popConsumerKVServiceEnable) { + this.popConsumerKVServiceEnable = popConsumerKVServiceEnable; + } + + public int getPopReviveMaxReturnSizePerRead() { + return popReviveMaxReturnSizePerRead; + } + + public void setPopReviveMaxReturnSizePerRead(int popReviveMaxReturnSizePerRead) { + this.popReviveMaxReturnSizePerRead = popReviveMaxReturnSizePerRead; + } + + public int getPopReviveMaxAttemptTimes() { + return popReviveMaxAttemptTimes; + } + + public void setPopReviveMaxAttemptTimes(int popReviveMaxAttemptTimes) { + this.popReviveMaxAttemptTimes = popReviveMaxAttemptTimes; + } + + public boolean isTraceOn() { + return traceOn; + } + + public void setTraceOn(final boolean traceOn) { + this.traceOn = traceOn; + } + + public long getStartAcceptSendRequestTimeStamp() { + return startAcceptSendRequestTimeStamp; + } + + public void setStartAcceptSendRequestTimeStamp(final long startAcceptSendRequestTimeStamp) { + this.startAcceptSendRequestTimeStamp = startAcceptSendRequestTimeStamp; + } + + public long getWaitTimeMillsInSendQueue() { + return waitTimeMillsInSendQueue; + } + + public void setWaitTimeMillsInSendQueue(final long waitTimeMillsInSendQueue) { + this.waitTimeMillsInSendQueue = waitTimeMillsInSendQueue; + } + + public long getConsumerFallbehindThreshold() { + return consumerFallbehindThreshold; + } + + public void setConsumerFallbehindThreshold(final long consumerFallbehindThreshold) { + this.consumerFallbehindThreshold = consumerFallbehindThreshold; + } + + public boolean isBrokerFastFailureEnable() { + return brokerFastFailureEnable; + } + + public void setBrokerFastFailureEnable(final boolean brokerFastFailureEnable) { + this.brokerFastFailureEnable = brokerFastFailureEnable; + } + + public long getWaitTimeMillsInPullQueue() { + return waitTimeMillsInPullQueue; + } + + public void setWaitTimeMillsInPullQueue(final long waitTimeMillsInPullQueue) { + this.waitTimeMillsInPullQueue = waitTimeMillsInPullQueue; + } + + public boolean isDisableConsumeIfConsumerReadSlowly() { + return disableConsumeIfConsumerReadSlowly; + } + + public void setDisableConsumeIfConsumerReadSlowly(final boolean disableConsumeIfConsumerReadSlowly) { + this.disableConsumeIfConsumerReadSlowly = disableConsumeIfConsumerReadSlowly; + } + + public boolean isSlaveReadEnable() { + return slaveReadEnable; + } + + public void setSlaveReadEnable(final boolean slaveReadEnable) { + this.slaveReadEnable = slaveReadEnable; + } + + public int getRegisterBrokerTimeoutMills() { + return registerBrokerTimeoutMills; + } + + public void setRegisterBrokerTimeoutMills(final int registerBrokerTimeoutMills) { + this.registerBrokerTimeoutMills = registerBrokerTimeoutMills; + } + + public String getRegionId() { + return regionId; + } + + public void setRegionId(final String regionId) { + this.regionId = regionId; + } + + public boolean isTransferMsgByHeap() { + return transferMsgByHeap; + } + + public void setTransferMsgByHeap(final boolean transferMsgByHeap) { + this.transferMsgByHeap = transferMsgByHeap; + } + + public String getMessageStorePlugIn() { + return messageStorePlugIn; + } + + public void setMessageStorePlugIn(String messageStorePlugIn) { + this.messageStorePlugIn = messageStorePlugIn; + } + + public boolean isHighSpeedMode() { + return highSpeedMode; + } + + public void setHighSpeedMode(final boolean highSpeedMode) { + this.highSpeedMode = highSpeedMode; + } + + public int getBrokerPermission() { + return brokerPermission; + } + + public void setBrokerPermission(int brokerPermission) { + this.brokerPermission = brokerPermission; + } + + public int getDefaultTopicQueueNums() { + return defaultTopicQueueNums; + } + + public void setDefaultTopicQueueNums(int defaultTopicQueueNums) { + this.defaultTopicQueueNums = defaultTopicQueueNums; + } + + public boolean isAutoCreateTopicEnable() { + return autoCreateTopicEnable; + } + + public void setAutoCreateTopicEnable(boolean autoCreateTopic) { + this.autoCreateTopicEnable = autoCreateTopic; + } + + public String getBrokerIP1() { + return brokerIP1; + } + + public void setBrokerIP1(String brokerIP1) { + this.brokerIP1 = brokerIP1; + } + + public String getBrokerIP2() { + return brokerIP2; + } + + public void setBrokerIP2(String brokerIP2) { + this.brokerIP2 = brokerIP2; + } + + public int getSendMessageThreadPoolNums() { + return sendMessageThreadPoolNums; + } + + 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; + } + + 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; + } + + public void setQueryMessageThreadPoolNums(final int queryMessageThreadPoolNums) { + this.queryMessageThreadPoolNums = queryMessageThreadPoolNums; + } + + public int getAdminBrokerThreadPoolNums() { + return adminBrokerThreadPoolNums; + } + + public void setAdminBrokerThreadPoolNums(int adminBrokerThreadPoolNums) { + this.adminBrokerThreadPoolNums = adminBrokerThreadPoolNums; + } + + public int getFlushConsumerOffsetInterval() { + return flushConsumerOffsetInterval; + } + + public void setFlushConsumerOffsetInterval(int flushConsumerOffsetInterval) { + this.flushConsumerOffsetInterval = flushConsumerOffsetInterval; + } + + public int getFlushConsumerOffsetHistoryInterval() { + return flushConsumerOffsetHistoryInterval; + } + + public void setFlushConsumerOffsetHistoryInterval(int flushConsumerOffsetHistoryInterval) { + this.flushConsumerOffsetHistoryInterval = flushConsumerOffsetHistoryInterval; + } + + public boolean isClusterTopicEnable() { + return clusterTopicEnable; + } + + public void setClusterTopicEnable(boolean clusterTopicEnable) { + this.clusterTopicEnable = clusterTopicEnable; + } + + public String getNamesrvAddr() { + return namesrvAddr; + } + + public void setNamesrvAddr(String namesrvAddr) { + this.namesrvAddr = namesrvAddr; + } + + public boolean isAutoCreateSubscriptionGroup() { + return autoCreateSubscriptionGroup; + } + + public void setAutoCreateSubscriptionGroup(boolean autoCreateSubscriptionGroup) { + this.autoCreateSubscriptionGroup = autoCreateSubscriptionGroup; + } + + public String getBrokerConfigPath() { + return brokerConfigPath; + } + + public void setBrokerConfigPath(String brokerConfigPath) { + this.brokerConfigPath = brokerConfigPath; + } + + public String getRocketmqHome() { + return rocketmqHome; + } + + public void setRocketmqHome(String rocketmqHome) { + this.rocketmqHome = rocketmqHome; + } + + public int getListenPort() { + return listenPort; + } + + public void setListenPort(int listenPort) { + this.listenPort = listenPort; + } + + public int getLitePullMessageThreadPoolNums() { + return litePullMessageThreadPoolNums; + } + + public void setLitePullMessageThreadPoolNums(int litePullMessageThreadPoolNums) { + this.litePullMessageThreadPoolNums = litePullMessageThreadPoolNums; + } + + public int getLitePullThreadPoolQueueCapacity() { + return litePullThreadPoolQueueCapacity; + } + + public void setLitePullThreadPoolQueueCapacity(int litePullThreadPoolQueueCapacity) { + this.litePullThreadPoolQueueCapacity = litePullThreadPoolQueueCapacity; + } + + public int getAdminBrokerThreadPoolQueueCapacity() { + return adminBrokerThreadPoolQueueCapacity; + } + + public void setAdminBrokerThreadPoolQueueCapacity(int adminBrokerThreadPoolQueueCapacity) { + this.adminBrokerThreadPoolQueueCapacity = adminBrokerThreadPoolQueueCapacity; + } + + public int getLoadBalanceThreadPoolQueueCapacity() { + return loadBalanceThreadPoolQueueCapacity; + } + + public void setLoadBalanceThreadPoolQueueCapacity(int loadBalanceThreadPoolQueueCapacity) { + this.loadBalanceThreadPoolQueueCapacity = loadBalanceThreadPoolQueueCapacity; + } + + public int getSendHeartbeatTimeoutMillis() { + return sendHeartbeatTimeoutMillis; + } + + public void setSendHeartbeatTimeoutMillis(int sendHeartbeatTimeoutMillis) { + this.sendHeartbeatTimeoutMillis = sendHeartbeatTimeoutMillis; + } + + public long getWaitTimeMillsInLitePullQueue() { + return waitTimeMillsInLitePullQueue; + } + + public void setWaitTimeMillsInLitePullQueue(long waitTimeMillsInLitePullQueue) { + this.waitTimeMillsInLitePullQueue = waitTimeMillsInLitePullQueue; + } + + public boolean isLitePullMessageEnable() { + return litePullMessageEnable; + } + + public void setLitePullMessageEnable(boolean litePullMessageEnable) { + this.litePullMessageEnable = litePullMessageEnable; + } + + public int getSyncBrokerMemberGroupPeriod() { + return syncBrokerMemberGroupPeriod; + } + + 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 int getQueryThreadPoolQueueCapacity() { + return queryThreadPoolQueueCapacity; + } + + public void setQueryThreadPoolQueueCapacity(final int queryThreadPoolQueueCapacity) { + this.queryThreadPoolQueueCapacity = queryThreadPoolQueueCapacity; + } + + public boolean isBrokerTopicEnable() { + return brokerTopicEnable; + } + + public void setBrokerTopicEnable(boolean brokerTopicEnable) { + this.brokerTopicEnable = brokerTopicEnable; + } + + public boolean isLongPollingEnable() { + return longPollingEnable; + } + + public void setLongPollingEnable(boolean longPollingEnable) { + this.longPollingEnable = longPollingEnable; + } + + public boolean isNotifyConsumerIdsChangedEnable() { + return notifyConsumerIdsChangedEnable; + } + + public void setNotifyConsumerIdsChangedEnable(boolean notifyConsumerIdsChangedEnable) { + this.notifyConsumerIdsChangedEnable = notifyConsumerIdsChangedEnable; + } + + public long getShortPollingTimeMills() { + return shortPollingTimeMills; + } + + public void setShortPollingTimeMills(long shortPollingTimeMills) { + this.shortPollingTimeMills = shortPollingTimeMills; + } + + public int getClientManageThreadPoolNums() { + return clientManageThreadPoolNums; + } + + public void setClientManageThreadPoolNums(int clientManageThreadPoolNums) { + this.clientManageThreadPoolNums = clientManageThreadPoolNums; + } + + public int getClientManagerThreadPoolQueueCapacity() { + return clientManagerThreadPoolQueueCapacity; + } + + public void setClientManagerThreadPoolQueueCapacity(int clientManagerThreadPoolQueueCapacity) { + this.clientManagerThreadPoolQueueCapacity = clientManagerThreadPoolQueueCapacity; + } + + public int getConsumerManagerThreadPoolQueueCapacity() { + return consumerManagerThreadPoolQueueCapacity; + } + + public void setConsumerManagerThreadPoolQueueCapacity(int consumerManagerThreadPoolQueueCapacity) { + this.consumerManagerThreadPoolQueueCapacity = consumerManagerThreadPoolQueueCapacity; + } + + public int getConsumerManageThreadPoolNums() { + return consumerManageThreadPoolNums; + } + + public void setConsumerManageThreadPoolNums(int consumerManageThreadPoolNums) { + this.consumerManageThreadPoolNums = consumerManageThreadPoolNums; + } + + public int getCommercialBaseCount() { + return commercialBaseCount; + } + + public void setCommercialBaseCount(int commercialBaseCount) { + this.commercialBaseCount = commercialBaseCount; + } + + public boolean isEnableCalcFilterBitMap() { + return enableCalcFilterBitMap; + } + + public void setEnableCalcFilterBitMap(boolean enableCalcFilterBitMap) { + this.enableCalcFilterBitMap = enableCalcFilterBitMap; + } + + public int getExpectConsumerNumUseFilter() { + return expectConsumerNumUseFilter; + } + + public void setExpectConsumerNumUseFilter(int expectConsumerNumUseFilter) { + this.expectConsumerNumUseFilter = expectConsumerNumUseFilter; + } + + public int getMaxErrorRateOfBloomFilter() { + return maxErrorRateOfBloomFilter; + } + + public void setMaxErrorRateOfBloomFilter(int maxErrorRateOfBloomFilter) { + this.maxErrorRateOfBloomFilter = maxErrorRateOfBloomFilter; + } + + public long getFilterDataCleanTimeSpan() { + return filterDataCleanTimeSpan; + } + + public void setFilterDataCleanTimeSpan(long filterDataCleanTimeSpan) { + this.filterDataCleanTimeSpan = filterDataCleanTimeSpan; + } + + public boolean isFilterSupportRetry() { + return filterSupportRetry; + } + + public void setFilterSupportRetry(boolean filterSupportRetry) { + this.filterSupportRetry = filterSupportRetry; + } + + public boolean isEnablePropertyFilter() { + return enablePropertyFilter; + } + + 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 long getWaitTimeMillsInAdminBrokerQueue() { + return waitTimeMillsInAdminBrokerQueue; + } + + public void setWaitTimeMillsInAdminBrokerQueue(long waitTimeMillsInAdminBrokerQueue) { + this.waitTimeMillsInAdminBrokerQueue = waitTimeMillsInAdminBrokerQueue; + } + + public void setMsgTraceTopicName(String msgTraceTopicName) { + this.msgTraceTopicName = msgTraceTopicName; + } + + public boolean isTraceTopicEnable() { + return traceTopicEnable; + } + + public void setTraceTopicEnable(boolean traceTopicEnable) { + this.traceTopicEnable = traceTopicEnable; + } + + 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 long getPopLongPollingForceNotifyInterval() { + return popLongPollingForceNotifyInterval; + } + + public void setPopLongPollingForceNotifyInterval(long popLongPollingForceNotifyInterval) { + this.popLongPollingForceNotifyInterval = popLongPollingForceNotifyInterval; + } + + public boolean isEnableNotifyBeforePopCalculateLag() { + return enableNotifyBeforePopCalculateLag; + } + + public void setEnableNotifyBeforePopCalculateLag(boolean enableNotifyBeforePopCalculateLag) { + this.enableNotifyBeforePopCalculateLag = enableNotifyBeforePopCalculateLag; + } + + 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 int getMaxStatsIdleTimeInMinutes() { + return maxStatsIdleTimeInMinutes; + } + + public void setMaxStatsIdleTimeInMinutes(int maxStatsIdleTimeInMinutes) { + this.maxStatsIdleTimeInMinutes = maxStatsIdleTimeInMinutes; + } + + 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 boolean isEnableFastChannelEventProcess() { + return enableFastChannelEventProcess; + } + + public void setEnableFastChannelEventProcess(boolean enableFastChannelEventProcess) { + this.enableFastChannelEventProcess = enableFastChannelEventProcess; + } + + public boolean isPrintChannelGroups() { + return printChannelGroups; + } + + public void setPrintChannelGroups(boolean printChannelGroups) { + this.printChannelGroups = printChannelGroups; + } + + public int getPrintChannelGroupsMinNum() { + return printChannelGroupsMinNum; + } + + public void setPrintChannelGroupsMinNum(int printChannelGroupsMinNum) { + this.printChannelGroupsMinNum = printChannelGroupsMinNum; + } + + 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; + } + + public boolean isSkipWhenCKRePutReachMaxTimes() { + return skipWhenCKRePutReachMaxTimes; + } + + public void setSkipWhenCKRePutReachMaxTimes(boolean skipWhenCKRePutReachMaxTimes) { + this.skipWhenCKRePutReachMaxTimes = skipWhenCKRePutReachMaxTimes; + } + + public int getUpdateNameServerAddrPeriod() { + return updateNameServerAddrPeriod; + } + + public void setUpdateNameServerAddrPeriod(int updateNameServerAddrPeriod) { + this.updateNameServerAddrPeriod = updateNameServerAddrPeriod; + } + + public boolean isAppendAckAsync() { + return appendAckAsync; + } + + public void setAppendAckAsync(boolean appendAckAsync) { + this.appendAckAsync = appendAckAsync; + } + + public boolean isAppendCkAsync() { + return appendCkAsync; + } + + public void setAppendCkAsync(boolean appendCkAsync) { + this.appendCkAsync = appendCkAsync; + } + + public boolean isClearRetryTopicWhenDeleteTopic() { + return clearRetryTopicWhenDeleteTopic; + } + + public void setClearRetryTopicWhenDeleteTopic(boolean clearRetryTopicWhenDeleteTopic) { + this.clearRetryTopicWhenDeleteTopic = clearRetryTopicWhenDeleteTopic; + } + + public boolean isEnableLmqStats() { + return enableLmqStats; + } + + public void setEnableLmqStats(boolean enableLmqStats) { + this.enableLmqStats = enableLmqStats; + } + + public String getConfigManagerVersion() { + return configManagerVersion; + } + + public void setConfigManagerVersion(String configManagerVersion) { + this.configManagerVersion = configManagerVersion; + } + + public boolean isAllowRecallWhenBrokerNotWriteable() { + return allowRecallWhenBrokerNotWriteable; + } + + public void setAllowRecallWhenBrokerNotWriteable(boolean allowRecallWhenBrokerNotWriteable) { + this.allowRecallWhenBrokerNotWriteable = allowRecallWhenBrokerNotWriteable; + } + + public boolean isRecallMessageEnable() { + return recallMessageEnable; + } + + public void setRecallMessageEnable(boolean recallMessageEnable) { + this.recallMessageEnable = recallMessageEnable; + } + + public boolean isEnableRegisterProducer() { + return enableRegisterProducer; + } + + public void setEnableRegisterProducer(boolean enableRegisterProducer) { + this.enableRegisterProducer = enableRegisterProducer; + } + + public boolean isEnableCreateSysGroup() { + return enableCreateSysGroup; + } + + public void setEnableCreateSysGroup(boolean enableCreateSysGroup) { + this.enableCreateSysGroup = enableCreateSysGroup; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/BrokerConfigSingleton.java b/common/src/main/java/org/apache/rocketmq/common/BrokerConfigSingleton.java new file mode 100644 index 0000000..b77d2b4 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/BrokerConfigSingleton.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.common; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class BrokerConfigSingleton { + private static AtomicBoolean isInit = new AtomicBoolean(); + private static BrokerConfig brokerConfig; + + public static BrokerConfig getBrokerConfig() { + if (brokerConfig == null) { + throw new IllegalArgumentException("brokerConfig Cannot be null !"); + } + return brokerConfig; + } + + public static void setBrokerConfig(BrokerConfig brokerConfig) { + if (!isInit.compareAndSet(false, true)) { + throw new IllegalArgumentException("broker config have inited !"); + } + BrokerConfigSingleton.brokerConfig = brokerConfig; + } +} 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 0000000..e85a3aa --- /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/CheckRocksdbCqWriteResult.java b/common/src/main/java/org/apache/rocketmq/common/CheckRocksdbCqWriteResult.java new file mode 100644 index 0000000..fc67df8 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/CheckRocksdbCqWriteResult.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; + +public class CheckRocksdbCqWriteResult { + String checkResult; + + int checkStatus; + + public enum CheckStatus { + CHECK_OK(0), + CHECK_NOT_OK(1), + CHECK_IN_PROGRESS(2), + CHECK_ERROR(3); + + private int value; + + CheckStatus(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + + public String getCheckResult() { + return checkResult; + } + + public void setCheckResult(String checkResult) { + this.checkResult = checkResult; + } + + public int getCheckStatus() { + return checkStatus; + } + + public void setCheckStatus(int checkStatus) { + this.checkStatus = checkStatus; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/ConfigManager.java b/common/src/main/java/org/apache/rocketmq/common/ConfigManager.java new file mode 100644 index 0000000..3fcf466 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/ConfigManager.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.common; + +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.IOException; +import java.util.Map; + +public abstract class ConfigManager { + private static final Logger log = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); + + public boolean load() { + String fileName = null; + try { + fileName = this.configFilePath(); + String jsonString = MixAll.file2String(fileName); + + if (null == jsonString || jsonString.length() == 0) { + return this.loadBak(); + } else { + this.decode(jsonString); + log.info("load " + fileName + " OK"); + return true; + } + } catch (Exception e) { + log.error("load " + fileName + " failed, and try to load backup file", e); + return this.loadBak(); + } + } + + private boolean loadBak() { + String fileName = null; + try { + fileName = this.configFilePath() + ".bak"; + String jsonString = MixAll.file2String(fileName); + if (jsonString != null && jsonString.length() > 0) { + this.decode(jsonString); + log.info("load " + fileName + " OK"); + return true; + } + } catch (Exception e) { + log.error("load " + fileName + " Failed", e); + return false; + } + + return true; + } + + 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); + if (jsonString != null) { + String fileName = this.configFilePath(); + try { + MixAll.string2File(jsonString, fileName); + } catch (IOException e) { + log.error("persist file " + fileName + " exception", e); + } + } + } + + 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); +} 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 0000000..85606fc --- /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 new file mode 100644 index 0000000..4e43e5c --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/CountDownLatch2.java @@ -0,0 +1,197 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.locks.AbstractQueuedSynchronizer; + +/** + * Add reset feature for @see java.util.concurrent.CountDownLatch + */ +public class CountDownLatch2 { + private final Sync sync; + + /** + * Constructs a {@code CountDownLatch2} initialized with the given count. + * + * @param count the number of times {@link #countDown} must be invoked before threads can pass through {@link + * #await} + * @throws IllegalArgumentException if {@code count} is negative + */ + public CountDownLatch2(int count) { + if (count < 0) + throw new IllegalArgumentException("count < 0"); + this.sync = new Sync(count); + } + + /** + * Causes the current thread to wait until the latch has counted down to + * zero, unless the thread is {@linkplain Thread#interrupt interrupted}. + * + *

    If the current count is zero then this method returns immediately. + * + *

    If the current count is greater than zero then the current + * thread becomes disabled for thread scheduling purposes and lies + * dormant until one of two things happen: + *

      + *
    • The count reaches zero due to invocations of the + * {@link #countDown} method; or + *
    • Some other thread {@linkplain Thread#interrupt interrupts} + * the current thread. + *
    + * + *

    If the current thread: + *

      + *
    • has its interrupted status set on entry to this method; or + *
    • is {@linkplain Thread#interrupt interrupted} while waiting, + *
    + * then {@link InterruptedException} is thrown and the current thread's + * interrupted status is cleared. + * + * @throws InterruptedException if the current thread is interrupted while waiting + */ + public void await() throws InterruptedException { + sync.acquireSharedInterruptibly(1); + } + + /** + * Causes the current thread to wait until the latch has counted down to + * zero, unless the thread is {@linkplain Thread#interrupt interrupted}, + * or the specified waiting time elapses. + * + *

    If the current count is zero then this method returns immediately + * with the value {@code true}. + * + *

    If the current count is greater than zero then the current + * thread becomes disabled for thread scheduling purposes and lies + * dormant until one of three things happen: + *

      + *
    • The count reaches zero due to invocations of the + * {@link #countDown} method; or + *
    • Some other thread {@linkplain Thread#interrupt interrupts} + * the current thread; or + *
    • The specified waiting time elapses. + *
    + * + *

    If the count reaches zero then the method returns with the + * value {@code true}. + * + *

    If the current thread: + *

      + *
    • has its interrupted status set on entry to this method; or + *
    • is {@linkplain Thread#interrupt interrupted} while waiting, + *
    + * then {@link InterruptedException} is thrown and the current thread's + * interrupted status is cleared. + * + *

    If the specified waiting time elapses then the value {@code false} + * is returned. If the time is less than or equal to zero, the method + * will not wait at all. + * + * @param timeout the maximum time to wait + * @param unit the time unit of the {@code timeout} argument + * @return {@code true} if the count reached zero and {@code false} if the waiting time elapsed before the count + * reached zero + * @throws InterruptedException if the current thread is interrupted while waiting + */ + public boolean await(long timeout, TimeUnit unit) + throws InterruptedException { + return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout)); + } + + /** + * Decrements the count of the latch, releasing all waiting threads if + * the count reaches zero. + * + *

    If the current count is greater than zero then it is decremented. + * If the new count is zero then all waiting threads are re-enabled for + * thread scheduling purposes. + * + *

    If the current count equals zero then nothing happens. + */ + public void countDown() { + sync.releaseShared(1); + } + + /** + * Returns the current count. + * + *

    This method is typically used for debugging and testing purposes. + * + * @return the current count + */ + public long getCount() { + return sync.getCount(); + } + + public void reset() { + sync.reset(); + } + + /** + * Returns a string identifying this latch, as well as its state. + * The state, in brackets, includes the String {@code "Count ="} + * followed by the current count. + * + * @return a string identifying this latch, as well as its state + */ + public String toString() { + return super.toString() + "[Count = " + sync.getCount() + "]"; + } + + /** + * Synchronization control For CountDownLatch2. + * Uses AQS state to represent count. + */ + private static final class Sync extends AbstractQueuedSynchronizer { + private static final long serialVersionUID = 4982264981922014374L; + + private final int startCount; + + Sync(int count) { + this.startCount = count; + setState(count); + } + + 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 (; ; ) { + int c = getState(); + if (c == 0) + return false; + int nextc = c - 1; + if (compareAndSetState(c, nextc)) + return nextc == 0; + } + } + + protected void reset() { + setState(startCount); + } + } +} 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 0000000..072d0ca --- /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 0000000..910a73b --- /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 0000000..a4fe5da --- /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 0000000..9abf367 --- /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 new file mode 100644 index 0000000..20218f5 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/MQVersion.java @@ -0,0 +1,945 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 MQVersion { + + public static final int CURRENT_VERSION = Version.V5_3_3.ordinal(); + + public static String getVersionDesc(int value) { + int length = Version.values().length; + if (value >= length) { + return Version.values()[length - 1].name(); + } + + return Version.values()[value].name(); + } + + public static Version value2Version(int value) { + int length = Version.values().length; + if (value >= length) { + return Version.values()[length - 1]; + } + + return Version.values()[value]; + } + + public enum Version { + V3_0_0_SNAPSHOT, + V3_0_0_ALPHA1, + V3_0_0_BETA1, + V3_0_0_BETA2, + V3_0_0_BETA3, + V3_0_0_BETA4, + V3_0_0_BETA5, + V3_0_0_BETA6_SNAPSHOT, + V3_0_0_BETA6, + V3_0_0_BETA7_SNAPSHOT, + V3_0_0_BETA7, + V3_0_0_BETA8_SNAPSHOT, + V3_0_0_BETA8, + V3_0_0_BETA9_SNAPSHOT, + V3_0_0_BETA9, + V3_0_0_FINAL, + V3_0_1_SNAPSHOT, + V3_0_1, + V3_0_2_SNAPSHOT, + V3_0_2, + V3_0_3_SNAPSHOT, + V3_0_3, + V3_0_4_SNAPSHOT, + V3_0_4, + V3_0_5_SNAPSHOT, + V3_0_5, + V3_0_6_SNAPSHOT, + V3_0_6, + V3_0_7_SNAPSHOT, + V3_0_7, + V3_0_8_SNAPSHOT, + V3_0_8, + V3_0_9_SNAPSHOT, + V3_0_9, + + V3_0_10_SNAPSHOT, + V3_0_10, + + V3_0_11_SNAPSHOT, + V3_0_11, + + V3_0_12_SNAPSHOT, + V3_0_12, + + V3_0_13_SNAPSHOT, + V3_0_13, + + V3_0_14_SNAPSHOT, + V3_0_14, + + V3_0_15_SNAPSHOT, + V3_0_15, + + V3_1_0_SNAPSHOT, + V3_1_0, + + V3_1_1_SNAPSHOT, + V3_1_1, + + V3_1_2_SNAPSHOT, + V3_1_2, + + V3_1_3_SNAPSHOT, + V3_1_3, + + V3_1_4_SNAPSHOT, + V3_1_4, + + V3_1_5_SNAPSHOT, + V3_1_5, + + V3_1_6_SNAPSHOT, + V3_1_6, + + V3_1_7_SNAPSHOT, + V3_1_7, + + V3_1_8_SNAPSHOT, + V3_1_8, + + V3_1_9_SNAPSHOT, + V3_1_9, + + V3_2_0_SNAPSHOT, + V3_2_0, + + V3_2_1_SNAPSHOT, + V3_2_1, + + V3_2_2_SNAPSHOT, + V3_2_2, + + V3_2_3_SNAPSHOT, + V3_2_3, + + V3_2_4_SNAPSHOT, + V3_2_4, + + V3_2_5_SNAPSHOT, + V3_2_5, + + V3_2_6_SNAPSHOT, + V3_2_6, + + V3_2_7_SNAPSHOT, + V3_2_7, + + V3_2_8_SNAPSHOT, + V3_2_8, + + V3_2_9_SNAPSHOT, + V3_2_9, + + V3_3_1_SNAPSHOT, + V3_3_1, + + V3_3_2_SNAPSHOT, + V3_3_2, + + V3_3_3_SNAPSHOT, + V3_3_3, + + V3_3_4_SNAPSHOT, + V3_3_4, + + V3_3_5_SNAPSHOT, + V3_3_5, + + V3_3_6_SNAPSHOT, + V3_3_6, + + V3_3_7_SNAPSHOT, + V3_3_7, + + V3_3_8_SNAPSHOT, + V3_3_8, + + V3_3_9_SNAPSHOT, + V3_3_9, + + V3_4_1_SNAPSHOT, + V3_4_1, + + V3_4_2_SNAPSHOT, + V3_4_2, + + V3_4_3_SNAPSHOT, + V3_4_3, + + V3_4_4_SNAPSHOT, + V3_4_4, + + V3_4_5_SNAPSHOT, + V3_4_5, + + V3_4_6_SNAPSHOT, + V3_4_6, + + V3_4_7_SNAPSHOT, + V3_4_7, + + V3_4_8_SNAPSHOT, + V3_4_8, + + V3_4_9_SNAPSHOT, + V3_4_9, + V3_5_1_SNAPSHOT, + V3_5_1, + + V3_5_2_SNAPSHOT, + V3_5_2, + + V3_5_3_SNAPSHOT, + V3_5_3, + + V3_5_4_SNAPSHOT, + V3_5_4, + + V3_5_5_SNAPSHOT, + V3_5_5, + + V3_5_6_SNAPSHOT, + V3_5_6, + + V3_5_7_SNAPSHOT, + V3_5_7, + + V3_5_8_SNAPSHOT, + V3_5_8, + + V3_5_9_SNAPSHOT, + V3_5_9, + + V3_6_1_SNAPSHOT, + V3_6_1, + + V3_6_2_SNAPSHOT, + V3_6_2, + + V3_6_3_SNAPSHOT, + V3_6_3, + + V3_6_4_SNAPSHOT, + V3_6_4, + + V3_6_5_SNAPSHOT, + V3_6_5, + + V3_6_6_SNAPSHOT, + V3_6_6, + + V3_6_7_SNAPSHOT, + V3_6_7, + + V3_6_8_SNAPSHOT, + V3_6_8, + + V3_6_9_SNAPSHOT, + V3_6_9, + + V3_7_1_SNAPSHOT, + V3_7_1, + + V3_7_2_SNAPSHOT, + V3_7_2, + + V3_7_3_SNAPSHOT, + V3_7_3, + + V3_7_4_SNAPSHOT, + V3_7_4, + + V3_7_5_SNAPSHOT, + V3_7_5, + + V3_7_6_SNAPSHOT, + V3_7_6, + + V3_7_7_SNAPSHOT, + V3_7_7, + + V3_7_8_SNAPSHOT, + V3_7_8, + + V3_7_9_SNAPSHOT, + V3_7_9, + + V3_8_1_SNAPSHOT, + V3_8_1, + + V3_8_2_SNAPSHOT, + V3_8_2, + + V3_8_3_SNAPSHOT, + V3_8_3, + + V3_8_4_SNAPSHOT, + V3_8_4, + + V3_8_5_SNAPSHOT, + V3_8_5, + + V3_8_6_SNAPSHOT, + V3_8_6, + + V3_8_7_SNAPSHOT, + V3_8_7, + + V3_8_8_SNAPSHOT, + V3_8_8, + + V3_8_9_SNAPSHOT, + V3_8_9, + + V3_9_1_SNAPSHOT, + V3_9_1, + + V3_9_2_SNAPSHOT, + V3_9_2, + + V3_9_3_SNAPSHOT, + V3_9_3, + + V3_9_4_SNAPSHOT, + V3_9_4, + + V3_9_5_SNAPSHOT, + V3_9_5, + + V3_9_6_SNAPSHOT, + V3_9_6, + + V3_9_7_SNAPSHOT, + V3_9_7, + + V3_9_8_SNAPSHOT, + V3_9_8, + + V3_9_9_SNAPSHOT, + V3_9_9, + + V4_0_0_SNAPSHOT, + V4_0_0, + + V4_0_1_SNAPSHOT, + V4_0_1, + + V4_0_2_SNAPSHOT, + V4_0_2, + + V4_0_3_SNAPSHOT, + V4_0_3, + + V4_0_4_SNAPSHOT, + V4_0_4, + + V4_0_5_SNAPSHOT, + V4_0_5, + + V4_0_6_SNAPSHOT, + V4_0_6, + + V4_0_7_SNAPSHOT, + V4_0_7, + + V4_0_8_SNAPSHOT, + V4_0_8, + + V4_0_9_SNAPSHOT, + V4_0_9, + + V4_1_0_SNAPSHOT, + V4_1_0, + + V4_1_1_SNAPSHOT, + V4_1_1, + + V4_1_2_SNAPSHOT, + V4_1_2, + + V4_1_3_SNAPSHOT, + V4_1_3, + + V4_1_4_SNAPSHOT, + V4_1_4, + + V4_1_5_SNAPSHOT, + V4_1_5, + + V4_1_6_SNAPSHOT, + V4_1_6, + + V4_1_7_SNAPSHOT, + V4_1_7, + + V4_1_8_SNAPSHOT, + V4_1_8, + + V4_1_9_SNAPSHOT, + V4_1_9, + + V4_2_0_SNAPSHOT, + V4_2_0, + + V4_2_1_SNAPSHOT, + V4_2_1, + + V4_2_2_SNAPSHOT, + V4_2_2, + + V4_2_3_SNAPSHOT, + V4_2_3, + + V4_2_4_SNAPSHOT, + V4_2_4, + + V4_2_5_SNAPSHOT, + V4_2_5, + + V4_2_6_SNAPSHOT, + V4_2_6, + + V4_2_7_SNAPSHOT, + V4_2_7, + + V4_2_8_SNAPSHOT, + V4_2_8, + + V4_2_9_SNAPSHOT, + V4_2_9, + + V4_3_0_SNAPSHOT, + V4_3_0, + + V4_3_1_SNAPSHOT, + V4_3_1, + + V4_3_2_SNAPSHOT, + V4_3_2, + + V4_3_3_SNAPSHOT, + V4_3_3, + + V4_3_4_SNAPSHOT, + V4_3_4, + + V4_3_5_SNAPSHOT, + V4_3_5, + + V4_3_6_SNAPSHOT, + V4_3_6, + + V4_3_7_SNAPSHOT, + V4_3_7, + + V4_3_8_SNAPSHOT, + V4_3_8, + + V4_3_9_SNAPSHOT, + V4_3_9, + + V4_4_0_SNAPSHOT, + V4_4_0, + + V4_4_1_SNAPSHOT, + V4_4_1, + + V4_4_2_SNAPSHOT, + V4_4_2, + + V4_4_3_SNAPSHOT, + V4_4_3, + + V4_4_4_SNAPSHOT, + V4_4_4, + + V4_4_5_SNAPSHOT, + V4_4_5, + + V4_4_6_SNAPSHOT, + V4_4_6, + + V4_4_7_SNAPSHOT, + V4_4_7, + + V4_4_8_SNAPSHOT, + V4_4_8, + + V4_4_9_SNAPSHOT, + V4_4_9, + + V4_5_0_SNAPSHOT, + V4_5_0, + + V4_5_1_SNAPSHOT, + V4_5_1, + + V4_5_2_SNAPSHOT, + V4_5_2, + + V4_5_3_SNAPSHOT, + V4_5_3, + + V4_5_4_SNAPSHOT, + V4_5_4, + + V4_5_5_SNAPSHOT, + V4_5_5, + + V4_5_6_SNAPSHOT, + V4_5_6, + + V4_5_7_SNAPSHOT, + V4_5_7, + + V4_5_8_SNAPSHOT, + V4_5_8, + + V4_5_9_SNAPSHOT, + V4_5_9, + + V4_6_0_SNAPSHOT, + V4_6_0, + + V4_6_1_SNAPSHOT, + V4_6_1, + + V4_6_2_SNAPSHOT, + V4_6_2, + + V4_6_3_SNAPSHOT, + V4_6_3, + + V4_6_4_SNAPSHOT, + V4_6_4, + + V4_6_5_SNAPSHOT, + V4_6_5, + + V4_6_6_SNAPSHOT, + V4_6_6, + + V4_6_7_SNAPSHOT, + V4_6_7, + + V4_6_8_SNAPSHOT, + V4_6_8, + + V4_6_9_SNAPSHOT, + V4_6_9, + + V4_7_0_SNAPSHOT, + V4_7_0, + + V4_7_1_SNAPSHOT, + V4_7_1, + + V4_7_2_SNAPSHOT, + V4_7_2, + + V4_7_3_SNAPSHOT, + V4_7_3, + + V4_7_4_SNAPSHOT, + V4_7_4, + + V4_7_5_SNAPSHOT, + V4_7_5, + + V4_7_6_SNAPSHOT, + V4_7_6, + + V4_7_7_SNAPSHOT, + V4_7_7, + + V4_7_8_SNAPSHOT, + V4_7_8, + + V4_7_9_SNAPSHOT, + V4_7_9, + + V4_8_0_SNAPSHOT, + V4_8_0, + + V4_8_1_SNAPSHOT, + V4_8_1, + + V4_8_2_SNAPSHOT, + V4_8_2, + + V4_8_3_SNAPSHOT, + V4_8_3, + + V4_8_4_SNAPSHOT, + V4_8_4, + + V4_8_5_SNAPSHOT, + V4_8_5, + + V4_8_6_SNAPSHOT, + V4_8_6, + + V4_8_7_SNAPSHOT, + V4_8_7, + + V4_8_8_SNAPSHOT, + V4_8_8, + + V4_8_9_SNAPSHOT, + V4_8_9, + + V4_9_0_SNAPSHOT, + V4_9_0, + + V4_9_1_SNAPSHOT, + V4_9_1, + + V4_9_2_SNAPSHOT, + V4_9_2, + + V4_9_3_SNAPSHOT, + V4_9_3, + + V4_9_4_SNAPSHOT, + V4_9_4, + + V4_9_5_SNAPSHOT, + V4_9_5, + + V4_9_6_SNAPSHOT, + V4_9_6, + + V4_9_7_SNAPSHOT, + V4_9_7, + + V4_9_8_SNAPSHOT, + V4_9_8, + + V4_9_9_SNAPSHOT, + V4_9_9, + + V5_0_0_SNAPSHOT, + V5_0_0, + + V5_0_1_SNAPSHOT, + V5_0_1, + + V5_0_2_SNAPSHOT, + V5_0_2, + + V5_0_3_SNAPSHOT, + V5_0_3, + + V5_0_4_SNAPSHOT, + V5_0_4, + + V5_0_5_SNAPSHOT, + V5_0_5, + + V5_0_6_SNAPSHOT, + V5_0_6, + + V5_0_7_SNAPSHOT, + V5_0_7, + + V5_0_8_SNAPSHOT, + V5_0_8, + + V5_0_9_SNAPSHOT, + V5_0_9, + + V5_1_0_SNAPSHOT, + V5_1_0, + + V5_1_1_SNAPSHOT, + V5_1_1, + + V5_1_2_SNAPSHOT, + V5_1_2, + + V5_1_3_SNAPSHOT, + V5_1_3, + + V5_1_4_SNAPSHOT, + V5_1_4, + + V5_1_5_SNAPSHOT, + V5_1_5, + + V5_1_6_SNAPSHOT, + V5_1_6, + + V5_1_7_SNAPSHOT, + V5_1_7, + + V5_1_8_SNAPSHOT, + V5_1_8, + + V5_1_9_SNAPSHOT, + V5_1_9, + + V5_2_0_SNAPSHOT, + V5_2_0, + + V5_2_1_SNAPSHOT, + V5_2_1, + + V5_2_2_SNAPSHOT, + V5_2_2, + + V5_2_3_SNAPSHOT, + V5_2_3, + + V5_2_4_SNAPSHOT, + V5_2_4, + + V5_2_5_SNAPSHOT, + V5_2_5, + + V5_2_6_SNAPSHOT, + V5_2_6, + + V5_2_7_SNAPSHOT, + V5_2_7, + + V5_2_8_SNAPSHOT, + V5_2_8, + + V5_2_9_SNAPSHOT, + V5_2_9, + + V5_3_0_SNAPSHOT, + V5_3_0, + + V5_3_1_SNAPSHOT, + V5_3_1, + + V5_3_2_SNAPSHOT, + V5_3_2, + + V5_3_3_SNAPSHOT, + V5_3_3, + + V5_3_4_SNAPSHOT, + V5_3_4, + + V5_3_5_SNAPSHOT, + V5_3_5, + + V5_3_6_SNAPSHOT, + V5_3_6, + + V5_3_7_SNAPSHOT, + V5_3_7, + + V5_3_8_SNAPSHOT, + V5_3_8, + + V5_3_9_SNAPSHOT, + V5_3_9, + + V5_4_0_SNAPSHOT, + V5_4_0, + + V5_4_1_SNAPSHOT, + V5_4_1, + + V5_4_2_SNAPSHOT, + V5_4_2, + + V5_4_3_SNAPSHOT, + V5_4_3, + + V5_4_4_SNAPSHOT, + V5_4_4, + + V5_4_5_SNAPSHOT, + V5_4_5, + + V5_4_6_SNAPSHOT, + V5_4_6, + + V5_4_7_SNAPSHOT, + V5_4_7, + + V5_4_8_SNAPSHOT, + V5_4_8, + + V5_4_9_SNAPSHOT, + V5_4_9, + + V5_5_0_SNAPSHOT, + V5_5_0, + + V5_5_1_SNAPSHOT, + V5_5_1, + + V5_5_2_SNAPSHOT, + V5_5_2, + + V5_5_3_SNAPSHOT, + V5_5_3, + + V5_5_4_SNAPSHOT, + V5_5_4, + + V5_5_5_SNAPSHOT, + V5_5_5, + + V5_5_6_SNAPSHOT, + V5_5_6, + + V5_5_7_SNAPSHOT, + V5_5_7, + + V5_5_8_SNAPSHOT, + V5_5_8, + + V5_5_9_SNAPSHOT, + V5_5_9, + + V5_6_0_SNAPSHOT, + V5_6_0, + + V5_6_1_SNAPSHOT, + V5_6_1, + + V5_6_2_SNAPSHOT, + V5_6_2, + + V5_6_3_SNAPSHOT, + V5_6_3, + + V5_6_4_SNAPSHOT, + V5_6_4, + + V5_6_5_SNAPSHOT, + V5_6_5, + + V5_6_6_SNAPSHOT, + V5_6_6, + + V5_6_7_SNAPSHOT, + V5_6_7, + + V5_6_8_SNAPSHOT, + V5_6_8, + + V5_6_9_SNAPSHOT, + V5_6_9, + + V5_7_0_SNAPSHOT, + V5_7_0, + + V5_7_1_SNAPSHOT, + V5_7_1, + + V5_7_2_SNAPSHOT, + V5_7_2, + + V5_7_3_SNAPSHOT, + V5_7_3, + + V5_7_4_SNAPSHOT, + V5_7_4, + + V5_7_5_SNAPSHOT, + V5_7_5, + + V5_7_6_SNAPSHOT, + V5_7_6, + + V5_7_7_SNAPSHOT, + V5_7_7, + + V5_7_8_SNAPSHOT, + V5_7_8, + + V5_7_9_SNAPSHOT, + V5_7_9, + + V5_8_0_SNAPSHOT, + V5_8_0, + + V5_8_1_SNAPSHOT, + V5_8_1, + + V5_8_2_SNAPSHOT, + V5_8_2, + + V5_8_3_SNAPSHOT, + V5_8_3, + + V5_8_4_SNAPSHOT, + V5_8_4, + + V5_8_5_SNAPSHOT, + V5_8_5, + + V5_8_6_SNAPSHOT, + V5_8_6, + + V5_8_7_SNAPSHOT, + V5_8_7, + + V5_8_8_SNAPSHOT, + V5_8_8, + + V5_8_9_SNAPSHOT, + V5_8_9, + + V5_9_0_SNAPSHOT, + V5_9_0, + + V5_9_1_SNAPSHOT, + V5_9_1, + + V5_9_2_SNAPSHOT, + V5_9_2, + + V5_9_3_SNAPSHOT, + V5_9_3, + + V5_9_4_SNAPSHOT, + V5_9_4, + + V5_9_5_SNAPSHOT, + V5_9_5, + + V5_9_6_SNAPSHOT, + V5_9_6, + + V5_9_7_SNAPSHOT, + V5_9_7, + + V5_9_8_SNAPSHOT, + V5_9_8, + + V5_9_9_SNAPSHOT, + V5_9_9, + + HIGHER_VERSION + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/MixAll.java b/common/src/main/java/org/apache/rocketmq/common/MixAll.java new file mode 100644 index 0000000..aca9bd4 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/MixAll.java @@ -0,0 +1,560 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +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 com.google.common.collect.ImmutableSet; +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.apache.rocketmq.common.topic.TopicValidator; +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 { + 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"); + 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 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 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 int LMQ_QUEUE_ID = 0; + public static final String LMQ_DISPATCH_SEPARATOR = ","; + 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(); + + private static final Set PREDEFINE_GROUP_SET = ImmutableSet.of( + DEFAULT_CONSUMER_GROUP, + DEFAULT_PRODUCER_GROUP, + TOOLS_CONSUMER_GROUP, + SCHEDULE_CONSUMER_GROUP, + FILTERSRV_CONSUMER_GROUP, + MONITOR_CONSUMER_GROUP, + CLIENT_INNER_PRODUCER_GROUP, + SELF_TEST_PRODUCER_GROUP, + SELF_TEST_CONSUMER_GROUP, + ONS_HTTP_PROXY_GROUP, + CID_ONSAPI_PERMISSION_GROUP, + CID_ONSAPI_OWNER_GROUP, + CID_ONSAPI_PULL_GROUP, + CID_SYS_RMQ_TRANS + ); + + public static boolean isWindows() { + return OS.contains("win"); + } + + public static boolean isMac() { + return OS.contains("mac"); + } + + public static boolean isUnix() { + return OS.contains("nix") + || OS.contains("nux") + || OS.contains("aix"); + } + + public static boolean isSolaris() { + return OS.contains("sunos"); + } + + public static String getWSAddr() { + String wsDomainName = System.getProperty("rocketmq.namesrv.domain", DEFAULT_NAMESRV_ADDR_LOOKUP); + String wsDomainSubgroup = System.getProperty("rocketmq.namesrv.domain.subgroup", "nsaddr"); + String wsAddr = "http://" + wsDomainName + ":8080/rocketmq/" + wsDomainSubgroup; + if (wsDomainName.indexOf(":") > 0) { + wsAddr = "http://" + wsDomainName + "/rocketmq/" + wsDomainSubgroup; + } + return wsAddr; + } + + public static String getRetryTopic(final String consumerGroup) { + return RETRY_GROUP_TOPIC_PREFIX + consumerGroup; + } + + public static String getReplyTopic(final String clusterName) { + return clusterName + "_" + REPLY_TOPIC_POSTFIX; + } + + public static boolean isSysConsumerGroup(final String consumerGroup) { + return consumerGroup.startsWith(CID_RMQ_SYS_PREFIX); + } + + public static boolean isSysConsumerGroupAndEnableCreate(final String consumerGroup, final boolean isEnableCreateSysGroup) { + return isEnableCreateSysGroup && isSysConsumerGroup(consumerGroup); + } + + public static boolean isPredefinedGroup(final String consumerGroup) { + return PREDEFINE_GROUP_SET.contains(consumerGroup); + } + + public static String getDLQTopic(final String consumerGroup) { + return DLQ_GROUP_TOPIC_PREFIX + consumerGroup; + } + + public static String brokerVIPChannel(final boolean isChange, final String brokerAddr) { + if (isChange) { + 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; + } + } + + public static long getPID() { + String processName = java.lang.management.ManagementFactory.getRuntimeMXBean().getName(); + if (StringUtils.isNotEmpty(processName)) { + try { + return Long.parseLong(processName.split("@")[0]); + } catch (Exception e) { + return 0; + } + } + + return 0; + } + + public static synchronized void string2File(final String str, final String fileName) throws IOException { + + String bakFile = fileName + ".bak"; + String prevContent = file2String(fileName); + if (prevContent != null) { + string2FileNotSafe(prevContent, bakFile); + } + + string2FileNotSafe(str, fileName); + } + + public static void string2FileNotSafe(final String str, final String fileName) throws IOException { + File file = new File(fileName); + File fileParent = file.getParentFile(); + if (fileParent != null) { + fileParent.mkdirs(); + } + IOTinyUtils.writeStringToFile(file, str, DEFAULT_CHARSET); + } + + public static String file2String(final String fileName) throws IOException { + File file = new File(fileName); + return file2String(file); + } + + public static String file2String(final File file) throws IOException { + if (file.exists()) { + byte[] data = new byte[(int) file.length()]; + boolean result; + + try (FileInputStream inputStream = new FileInputStream(file)) { + int len = inputStream.read(data); + result = len == data.length; + } + + if (result) { + return new String(data, DEFAULT_CHARSET); + } + } + return null; + } + + public static String file2String(final URL url) { + InputStream in = null; + try { + URLConnection urlConnection = url.openConnection(); + urlConnection.setUseCaches(false); + in = urlConnection.getInputStream(); + int len = in.available(); + byte[] data = new byte[len]; + in.read(data, 0, len); + return new String(data, StandardCharsets.UTF_8); + } catch (Exception ignored) { + } finally { + if (null != in) { + try { + in.close(); + } catch (IOException ignored) { + } + } + } + + return null; + } + + public static void printObjectProperties(final Logger logger, final Object object) { + printObjectProperties(logger, object, false); + } + + public static void printObjectProperties(final Logger logger, final Object object, + final boolean onlyImportantField) { + Field[] fields = object.getClass().getDeclaredFields(); + for (Field field : fields) { + 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); + value = field.get(object); + if (null == value) { + value = ""; + } + } catch (IllegalAccessException e) { + log.error("Failed to obtain object properties", e); + } + + if (logger != null) { + logger.info(name + "=" + value); + } + } + } + } + } + + 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(); + 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"); + } + } + return sb.toString(); + } + + public static Properties string2Properties(final String str) { + Properties properties = new Properties(); + try { + InputStream in = new ByteArrayInputStream(str.getBytes(DEFAULT_CHARSET)); + properties.load(in); + } catch (Exception e) { + log.error("Failed to handle properties", e); + return null; + } + + return properties; + } + + public static Properties object2Properties(final Object object) { + Properties properties = new Properties(); + + 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 (objectClass == Object.class || objectClass.getSuperclass() == Object.class) { + break; + } + objectClass = objectClass.getSuperclass(); + } + + return properties; + } + + public static void properties2Object(final Properties p, final Object object) { + Method[] methods = object.getClass().getMethods(); + for (Method method : methods) { + String mn = method.getName(); + if (mn.startsWith("set")) { + try { + String tmp = mn.substring(4); + String first = mn.substring(3, 4); + + String key = first.toLowerCase() + tmp; + String property = p.getProperty(key); + if (property != null) { + Class[] pt = method.getParameterTypes(); + if (pt.length > 0) { + String cn = pt[0].getSimpleName(); + Object arg; + if (cn.equals("int") || cn.equals("Integer")) { + arg = Integer.parseInt(property); + } else if (cn.equals("long") || cn.equals("Long")) { + arg = Long.parseLong(property); + } else if (cn.equals("double") || cn.equals("Double")) { + arg = Double.parseDouble(property); + } else if (cn.equals("boolean") || cn.equals("Boolean")) { + arg = Boolean.parseBoolean(property); + } else if (cn.equals("float") || cn.equals("Float")) { + arg = Float.parseFloat(property); + } else if (cn.equals("String")) { + property = property.trim(); + arg = property; + } else { + continue; + } + method.invoke(object, arg); + } + } + } catch (Throwable ignored) { + } + } + } + } + + 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<>(); + try { + Enumeration enumeration = NetworkInterface.getNetworkInterfaces(); + while (enumeration.hasMoreElements()) { + NetworkInterface networkInterface = enumeration.nextElement(); + Enumeration addrs = networkInterface.getInetAddresses(); + while (addrs.hasMoreElements()) { + inetAddressList.add(addrs.nextElement().getHostAddress()); + } + } + } catch (SocketException e) { + throw new RuntimeException("get local inet address fail", e); + } + + return inetAddressList; + } + + private static String localhost() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (Throwable e) { + try { + String candidatesHost = getLocalhostByNetworkInterface(); + if (candidatesHost != null) + return candidatesHost; + + } catch (Exception ignored) { + } + + throw new RuntimeException("InetAddress java.net.InetAddress.getLocalHost() throws UnknownHostException" + FAQUrl.suggestTodo(FAQUrl.UNKNOWN_HOST_EXCEPTION), e); + } + } + + //Reverse logic comparing to RemotingUtil method, consider refactor in RocketMQ 5.0 + public static String getLocalhostByNetworkInterface() throws SocketException { + List candidatesHost = new ArrayList<>(); + Enumeration enumeration = NetworkInterface.getNetworkInterfaces(); + + while (enumeration.hasMoreElements()) { + NetworkInterface networkInterface = enumeration.nextElement(); + // Workaround for docker0 bridge + if ("docker0".equals(networkInterface.getName()) || !networkInterface.isUp()) { + continue; + } + Enumeration addrs = networkInterface.getInetAddresses(); + while (addrs.hasMoreElements()) { + InetAddress address = addrs.nextElement(); + if (address.isLoopbackAddress()) { + continue; + } + //ip4 higher priority + if (address instanceof Inet6Address) { + candidatesHost.add(address.getHostAddress()); + continue; + } + return address.getHostAddress(); + } + } + + if (!candidatesHost.isEmpty()) { + return candidatesHost.get(0); + } + + // Fallback to loopback + return localhost(); + } + + public static boolean compareAndIncreaseOnly(final AtomicLong target, final long value) { + long prev = target.get(); + while (value > prev) { + boolean updated = target.compareAndSet(prev, value); + if (updated) + return true; + + prev = target.get(); + } + + return false; + } + + public static String humanReadableByteCount(long bytes, boolean si) { + int unit = si ? 1000 : 1024; + if (bytes < unit) + return bytes + " B"; + int exp = (int) (Math.log(bytes) / Math.log(unit)); + String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i"); + 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 isSysConsumerGroupPullMessage(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; + } + + public static boolean topicAllowsLMQ(String topic) { + return !topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX) + && !topic.startsWith(TopicValidator.SYSTEM_TOPIC_PREFIX) + && !topic.equals(TopicValidator.RMQ_SYS_SCHEDULE_TOPIC); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/ObjectCreator.java b/common/src/main/java/org/apache/rocketmq/common/ObjectCreator.java new file mode 100644 index 0000000..14c6454 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/ObjectCreator.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; + +public interface ObjectCreator { + T create(Object... args); +} diff --git a/common/src/main/java/org/apache/rocketmq/common/Pair.java b/common/src/main/java/org/apache/rocketmq/common/Pair.java new file mode 100644 index 0000000..138ab50 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/Pair.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 java.io.Serializable; + +public class Pair implements Serializable { + private T1 object1; + private T2 object2; + + public Pair(T1 object1, T2 object2) { + this.object1 = object1; + this.object2 = object2; + } + + public static Pair of(T1 object1, T2 object2) { + return new Pair<>(object1, 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/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 0000000..2f979fa --- /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/ServiceState.java b/common/src/main/java/org/apache/rocketmq/common/ServiceState.java new file mode 100644 index 0000000..4253b5e --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/ServiceState.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; + +public enum ServiceState { + /** + * Service just created,not start + */ + CREATE_JUST, + /** + * Service Running + */ + RUNNING, + /** + * Service shutdown + */ + SHUTDOWN_ALREADY, + /** + * Service Start failure + */ + START_FAILED; +} diff --git a/common/src/main/java/org/apache/rocketmq/common/ServiceThread.java b/common/src/main/java/org/apache/rocketmq/common/ServiceThread.java new file mode 100644 index 0000000..96195d5 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/ServiceThread.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.common; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +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 ServiceThread implements Runnable { + protected static final Logger log = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); + + private static final long JOIN_TIME = 90 * 1000; + + 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() { + + } + + 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() { + this.shutdown(false); + } + + 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[{}] interrupt={} ", getServiceName(), interrupt); + + //if thead is waiting, wakeup it + wakeup(); + + try { + if (interrupt) { + this.thread.interrupt(); + } + + long beginTime = System.currentTimeMillis(); + if (!this.thread.isDaemon()) { + this.thread.join(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() { + return JOIN_TIME; + } + + public void makeStop() { + if (!started.get()) { + return; + } + this.stopped = true; + log.info("makestop thread[{}] ", this.getServiceName()); + } + + public void wakeup() { + if (hasNotified.compareAndSet(false, true)) { + waitPoint.countDown(); // notify + } + } + + protected void waitForRunning(long interval) { + if (hasNotified.compareAndSet(true, false)) { + this.onWaitEnd(); + return; + } + + //entry to wait + waitPoint.reset(); + + try { + waitPoint.await(interval, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + log.error("Interrupted", e); + } finally { + hasNotified.set(false); + this.onWaitEnd(); + } + } + + 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 0000000..5b00724 --- /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/SystemClock.java b/common/src/main/java/org/apache/rocketmq/common/SystemClock.java new file mode 100644 index 0000000..b7eb3a6 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/SystemClock.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 class SystemClock { + public long now() { + return System.currentTimeMillis(); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/ThreadFactoryImpl.java b/common/src/main/java/org/apache/rocketmq/common/ThreadFactoryImpl.java new file mode 100644 index 0000000..bb0d141 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/ThreadFactoryImpl.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 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) { + 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 0000000..c507748 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/TopicAttributes.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; + +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.LongRangeAttribute; +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 LongRangeAttribute TOPIC_RESERVE_TIME_ATTRIBUTE = new LongRangeAttribute( + "reserve.time", + true, + -1, + Long.MAX_VALUE, + -1 + ); + + 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); + ALL.put(TOPIC_RESERVE_TIME_ATTRIBUTE.getName(), TOPIC_RESERVE_TIME_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 new file mode 100644 index 0000000..0bf6490 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/TopicConfig.java @@ -0,0 +1,273 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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; + private int perm = PermName.PERM_READ | PermName.PERM_WRITE; + 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() { + } + + 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; + this.writeQueueNums = writeQueueNums; + 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.length >= 5) { + this.topicName = strs[0]; + + this.readQueueNums = Integer.parseInt(strs[1]); + + this.writeQueueNums = Integer.parseInt(strs[2]); + + this.perm = Integer.parseInt(strs[3]); + + 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; + } + + return false; + } + + public String getTopicName() { + return topicName; + } + + public void setTopicName(String topicName) { + this.topicName = topicName; + } + + public int getReadQueueNums() { + return readQueueNums; + } + + public void setReadQueueNums(int readQueueNums) { + this.readQueueNums = readQueueNums; + } + + public int getWriteQueueNums() { + return writeQueueNums; + } + + public void setWriteQueueNums(int writeQueueNums) { + this.writeQueueNums = writeQueueNums; + } + + public int getPerm() { + return perm; + } + + public void setPerm(int perm) { + this.perm = perm; + } + + public TopicFilterType getTopicFilterType() { + return topicFilterType; + } + + public void setTopicFilterType(TopicFilterType topicFilterType) { + this.topicFilterType = topicFilterType; + } + + public int getTopicSysFlag() { + return topicSysFlag; + } + + public void setTopicSysFlag(int topicSysFlag) { + this.topicSysFlag = topicSysFlag; + } + + public boolean isOrder() { + return order; + } + + 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(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TopicConfig that = (TopicConfig) o; + + if (readQueueNums != that.readQueueNums) { + return false; + } + if (writeQueueNums != that.writeQueueNums) { + return false; + } + if (perm != that.perm) { + return false; + } + if (topicSysFlag != that.topicSysFlag) { + return false; + } + if (order != that.order) { + return false; + } + if (!Objects.equals(topicName, that.topicName)) { + return false; + } + if (topicFilterType != that.topicFilterType) { + return false; + } + return Objects.equals(attributes, that.attributes); + } + + @Override + public int hashCode() { + int result = topicName != null ? topicName.hashCode() : 0; + result = 31 * result + readQueueNums; + result = 31 * result + writeQueueNums; + result = 31 * result + perm; + 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; + } + + @Override + public String toString() { + return "TopicConfig [topicName=" + topicName + ", readQueueNums=" + readQueueNums + + ", writeQueueNums=" + writeQueueNums + ", perm=" + PermName.perm2String(perm) + + ", topicFilterType=" + topicFilterType + ", topicSysFlag=" + topicSysFlag + ", order=" + order + + ", attributes=" + attributes + "]"; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/TopicFilterType.java b/common/src/main/java/org/apache/rocketmq/common/TopicFilterType.java new file mode 100644 index 0000000..8dde5d8 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/TopicFilterType.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 enum TopicFilterType { + SINGLE_TAG, + MULTI_TAG + +} 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 0000000..c5f0387 --- /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 0000000..e86ec8b --- /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 new file mode 100644 index 0000000..a42ac3f --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/UtilAll.java @@ -0,0 +1,760 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 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.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.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"; + 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() { + 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 { + timeUnit.sleep(timeOut); + } catch (Throwable ignored) { + + } + } + + public static String currentStackTrace() { + StringBuilder sb = new StringBuilder(); + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + for (StackTraceElement ste : stackTrace) { + sb.append("\n\t"); + sb.append(ste.toString()); + } + + return sb.toString(); + } + + public static String offset2FileName(final long offset) { + final NumberFormat nf = NumberFormat.getInstance(); + nf.setMinimumIntegerDigits(20); + nf.setMaximumFractionDigits(0); + nf.setGroupingUsed(false); + return nf.format(offset); + } + + public static long computeElapsedTimeMilliseconds(final long beginTime) { + return System.currentTimeMillis() - beginTime; + } + + public static boolean isItTimeToDo(final String when) { + String[] whiles = when.split(";"); + if (whiles.length > 0) { + Calendar now = Calendar.getInstance(); + for (String w : whiles) { + int nowHour = Integer.parseInt(w); + if (nowHour == now.get(Calendar.HOUR_OF_DAY)) { + return true; + } + } + } + + return false; + } + + public static String timeMillisToHumanString() { + return timeMillisToHumanString(System.currentTimeMillis()); + } + + public static String timeMillisToHumanString(final long t) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(t); + return String.format("%04d%02d%02d%02d%02d%02d%03d", cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, + cal.get(Calendar.DAY_OF_MONTH), cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND), + cal.get(Calendar.MILLISECOND)); + } + + public static long computeNextMorningTimeMillis() { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(System.currentTimeMillis()); + cal.add(Calendar.DAY_OF_MONTH, 1); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + + return cal.getTimeInMillis(); + } + + public static long computeNextMinutesTimeMillis() { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(System.currentTimeMillis()); + cal.add(Calendar.DAY_OF_MONTH, 0); + cal.add(Calendar.HOUR_OF_DAY, 0); + cal.add(Calendar.MINUTE, 1); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + + return cal.getTimeInMillis(); + } + + public static long computeNextHourTimeMillis() { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(System.currentTimeMillis()); + cal.add(Calendar.DAY_OF_MONTH, 0); + cal.add(Calendar.HOUR_OF_DAY, 1); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + + return cal.getTimeInMillis(); + } + + public static long computeNextHalfHourTimeMillis() { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(System.currentTimeMillis()); + cal.add(Calendar.DAY_OF_MONTH, 0); + cal.add(Calendar.HOUR_OF_DAY, 1); + cal.set(Calendar.MINUTE, 30); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + + return cal.getTimeInMillis(); + } + + public static String timeMillisToHumanString2(final long t) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(t); + return String.format("%04d-%02d-%02d %02d:%02d:%02d,%03d", + cal.get(Calendar.YEAR), + cal.get(Calendar.MONTH) + 1, + cal.get(Calendar.DAY_OF_MONTH), + cal.get(Calendar.HOUR_OF_DAY), + cal.get(Calendar.MINUTE), + cal.get(Calendar.SECOND), + cal.get(Calendar.MILLISECOND)); + } + + public static String timeMillisToHumanString3(final long t) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(t); + return String.format("%04d%02d%02d%02d%02d%02d", + cal.get(Calendar.YEAR), + cal.get(Calendar.MONTH) + 1, + cal.get(Calendar.DAY_OF_MONTH), + cal.get(Calendar.HOUR_OF_DAY), + cal.get(Calendar.MINUTE), + cal.get(Calendar.SECOND)); + } + + 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()) { + 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 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); + } + + return 0; + } + + public static int crc32(byte[] array, int offset, int length) { + CRC32 crc32 = new CRC32(); + crc32.update(array, offset, 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++) { + int v = src[j] & 0xFF; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + return new String(hexChars); + } + + public static void writeInt(char[] buffer, int pos, int value) { + for (int moveBits = 28; moveBits >= 0; moveBits -= 4) { + buffer[pos++] = HEX_ARRAY[(value >>> moveBits) & 0x0F]; + } + } + + public static void writeShort(char[] buffer, int pos, int value) { + for (int moveBits = 12; moveBits >= 0; moveBits -= 4) { + buffer[pos++] = HEX_ARRAY[(value >>> moveBits) & 0x0F]; + } + } + + public static byte[] string2bytes(String hexString) { + if (hexString == null || hexString.equals("")) { + return null; + } + hexString = hexString.toUpperCase(); + int length = hexString.length() / 2; + char[] hexChars = hexString.toCharArray(); + byte[] d = new byte[length]; + for (int i = 0; i < length; i++) { + int pos = i * 2; + d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1])); + } + return d; + } + + 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]; + 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; + } + + /** + * 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); + 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; + } + + public static int asInt(String str, int defaultValue) { + try { + return Integer.parseInt(str); + } catch (Exception e) { + return defaultValue; + } + } + + public static long asLong(String str, long defaultValue) { + try { + return Long.parseLong(str); + } catch (Exception e) { + return defaultValue; + } + } + + public static String formatDate(Date date, String pattern) { + SimpleDateFormat df = new SimpleDateFormat(pattern); + return df.format(date); + } + + public static Date parseDate(String date, String pattern) { + SimpleDateFormat df = new SimpleDateFormat(pattern); + try { + return df.parse(date); + } catch (ParseException e) { + return null; + } + } + + public static String responseCode2String(final int code) { + return Integer.toString(code); + } + + public static String frontStringAtLeast(final String str, final int size) { + if (str != null) { + if (str.length() > size) { + return str.substring(0, size); + } + } + + return str; + } + + public static boolean isBlank(String str) { + return StringUtils.isBlank(str); + } + + public static String jstack() { + return jstack(Thread.getAllStackTraces()); + } + + public static String jstack(Map map) { + StringBuilder result = new StringBuilder(); + try { + Iterator> ite = map.entrySet().iterator(); + while (ite.hasNext()) { + Map.Entry entry = ite.next(); + StackTraceElement[] elements = entry.getValue(); + Thread thread = entry.getKey(); + if (elements != null && elements.length > 0) { + String threadName = entry.getKey().getName(); + result.append(String.format("%-40sTID: %d STATE: %s%n", threadName, thread.getId(), thread.getState())); + for (StackTraceElement el : elements) { + result.append(String.format("%-40s%s%n", threadName, el.toString())); + } + result.append("\n"); + } + } + } catch (Throwable 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"); + } + + //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) { + return ip[1] >= (byte) 16 && ip[1] <= (byte) 31; + } else if (ip[0] == (byte) 192) { + return ip[1] == (byte) 168; + } + return false; + } + + public static boolean isInternalV6IP(InetAddress inetAddr) { + return 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... + } + + private static boolean ipCheck(byte[] ip) { + if (ip.length != 4) { + throw new RuntimeException("illegal ipv4 bytes"); + } + + InetAddressValidator validator = InetAddressValidator.getInstance(); + return validator.isValidInet4Address(ipToIPv4Str(ip)); + } + + private static boolean ipV6Check(byte[] ip) { + if (ip.length != 16) { + throw new RuntimeException("illegal ipv6 bytes"); + } + + InetAddressValidator validator = InetAddressValidator.getInstance(); + return validator.isValidInet6Address(ipToIPv6Str(ip)); + } + + public static String ipToIPv4Str(byte[] ip) { + if (ip.length != 4) { + return null; + } + return new StringBuilder().append(ip[0] & 0xFF).append(".").append( + 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(); + InetAddress ip; + byte[] internalIP = null; + while (allNetInterfaces.hasMoreElements()) { + NetworkInterface netInterface = allNetInterfaces.nextElement(); + Enumeration addresses = netInterface.getInetAddresses(); + while (addresses.hasMoreElements()) { + ip = addresses.nextElement(); + if (ip instanceof Inet4Address) { + byte[] ipByte = ip.getAddress(); + if (ipByte.length == 4) { + if (ipCheck(ipByte)) { + if (!isInternalIP(ipByte)) { + return ipByte; + } else if (internalIP == null || internalIP[0] == (byte) 127) { + internalIP = ipByte; + } + } + } + } else if (ip instanceof Inet6Address) { + byte[] ipByte = ip.getAddress(); + if (ipByte.length == 16) { + if (ipV6Check(ipByte)) { + if (!isInternalV6IP(ip)) { + return ipByte; + } + } + } + } + } + } + if (internalIP != null) { + return internalIP; + } else { + throw new RuntimeException("Can not get local ip"); + } + } catch (Exception e) { + throw new RuntimeException("Can not get local ip", e); + } + } + + 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(); + } + } + + 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/action/Action.java b/common/src/main/java/org/apache/rocketmq/common/action/Action.java new file mode 100644 index 0000000..7e12323 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/action/Action.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.action; + +import com.alibaba.fastjson2.annotation.JSONField; +import org.apache.commons.lang3.StringUtils; + +public enum Action { + + UNKNOWN((byte) 0, "Unknown"), + + ALL((byte) 1, "All"), + + ANY((byte) 2, "Any"), + + PUB((byte) 3, "Pub"), + + SUB((byte) 4, "Sub"), + + CREATE((byte) 5, "Create"), + + UPDATE((byte) 6, "Update"), + + DELETE((byte) 7, "Delete"), + + GET((byte) 8, "Get"), + + LIST((byte) 9, "List"); + + @JSONField(value = true) + private final byte code; + private final String name; + + Action(byte code, String name) { + this.code = code; + this.name = name; + } + + public static Action getByName(String name) { + for (Action action : Action.values()) { + if (StringUtils.equalsIgnoreCase(action.getName(), name)) { + return action; + } + } + return null; + } + + public byte getCode() { + return code; + } + + public String getName() { + return name; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/action/RocketMQAction.java b/common/src/main/java/org/apache/rocketmq/common/action/RocketMQAction.java new file mode 100644 index 0000000..251d9d5 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/action/RocketMQAction.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.action; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.apache.rocketmq.common.resource.ResourceType; + +@Retention(RetentionPolicy.RUNTIME) +public @interface RocketMQAction { + + int value(); + + ResourceType resource() default ResourceType.UNKNOWN; + + Action[] action(); +} diff --git a/common/src/main/java/org/apache/rocketmq/common/annotation/ImportantField.java b/common/src/main/java/org/apache/rocketmq/common/annotation/ImportantField.java new file mode 100644 index 0000000..6a54a33 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/annotation/ImportantField.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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE}) +public @interface ImportantField { +} 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 0000000..ba9be3b --- /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 0000000..da98c6d --- /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 0000000..a364698 --- /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 0000000..41ad748 --- /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 0000000..9148d5a --- /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 0000000..5f289a0 --- /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 0000000..5353b8a --- /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 0000000..eeeda72 --- /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 0000000..5e581a3 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/attribute/TopicMessageType.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.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 + || messageProperty.get(MessageConst.PROPERTY_TIMER_DELAY_MS) != 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/chain/Handler.java b/common/src/main/java/org/apache/rocketmq/common/chain/Handler.java new file mode 100644 index 0000000..0e2b4a4 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/chain/Handler.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.chain; + +public interface Handler { + + R handle(T t, HandlerChain chain); +} diff --git a/common/src/main/java/org/apache/rocketmq/common/chain/HandlerChain.java b/common/src/main/java/org/apache/rocketmq/common/chain/HandlerChain.java new file mode 100644 index 0000000..220689e --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/chain/HandlerChain.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.chain; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class HandlerChain { + + private List> handlers; + private Iterator> iterator; + + public static HandlerChain create() { + return new HandlerChain<>(); + } + + public HandlerChain addNext(Handler handler) { + if (this.handlers == null) { + this.handlers = new ArrayList<>(); + } + this.handlers.add(handler); + return this; + } + + public R handle(T t) { + if (iterator == null) { + iterator = handlers.iterator(); + } + if (iterator.hasNext()) { + Handler handler = iterator.next(); + return handler.handle(t, this); + } + return null; + } +} 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 0000000..212bc08 --- /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 0000000..d748fc4 --- /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 0000000..c900045 --- /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 0000000..aace1d9 --- /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 0000000..0bcb968 --- /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 0000000..e64db9b --- /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 0000000..131035c --- /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 0000000..6c0bce5 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/config/AbstractRocksDBStorage.java @@ -0,0 +1,593 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 com.google.common.collect.Maps; +import io.netty.buffer.PooledByteBufAllocator; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +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 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.rocksdb.ColumnFamilyDescriptor; +import org.rocksdb.ColumnFamilyHandle; +import org.rocksdb.ColumnFamilyOptions; +import org.rocksdb.CompactRangeOptions; +import org.rocksdb.CompactionOptions; +import org.rocksdb.CompressionType; +import org.rocksdb.DBOptions; +import org.rocksdb.Env; +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; + +public abstract class AbstractRocksDBStorage { + protected static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.ROCKSDB_LOGGER_NAME); + + /** + * Direct Jemalloc allocator + */ + public static final PooledByteBufAllocator POOLED_ALLOCATOR = new PooledByteBufAllocator(true); + + public static final byte CTRL_0 = '\u0000'; + public static final byte CTRL_1 = '\u0001'; + public static final byte CTRL_2 = '\u0002'; + + private static final String SPACE = " | "; + + protected final 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 final List cfHandles = new ArrayList<>(); + + protected volatile boolean loaded; + protected CompressionType compressionType = CompressionType.LZ4_COMPRESSION; + 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 AbstractRocksDBStorage(String dbPath) { + this.dbPath = dbPath; + } + + protected void initOptions() { + initWriteOptions(); + initAbleWalWriteOptions(); + initReadOptions(); + initTotalOrderReadOptions(); + initCompactRangeOptions(); + initCompactionOptions(); + } + + /** + * Write options for Atomic Flush + */ + protected void initWriteOptions() { + this.writeOptions = new WriteOptions(); + this.writeOptions.setSync(false); + this.writeOptions.setDisableWAL(true); + // https://github.com/facebook/rocksdb/wiki/Write-Stalls + this.writeOptions.setNoSlowdown(false); + } + + protected void initAbleWalWriteOptions() { + this.ableWalWriteOptions = new WriteOptions(); + this.ableWalWriteOptions.setSync(false); + this.ableWalWriteOptions.setDisableWAL(false); + // https://github.com/facebook/rocksdb/wiki/Write-Stalls + this.ableWalWriteOptions.setNoSlowdown(false); + } + + protected void initReadOptions() { + this.readOptions = new ReadOptions(); + this.readOptions.setPrefixSameAsStart(true); + this.readOptions.setTotalOrderSeek(false); + this.readOptions.setTailing(false); + } + + protected void initTotalOrderReadOptions() { + this.totalOrderReadOptions = new ReadOptions(); + this.totalOrderReadOptions.setPrefixSameAsStart(false); + this.totalOrderReadOptions.setTotalOrderSeek(true); + this.totalOrderReadOptions.setTailing(false); + } + + protected void initCompactRangeOptions() { + 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); + } + + protected void initCompactionOptions() { + this.compactionOptions = new CompactionOptions(); + this.compactionOptions.setCompression(compressionType); + this.compactionOptions.setMaxSubcompactions(4); + this.compactionOptions.setOutputFileSizeLimit(4 * 1024 * 1024 * 1024L); + } + + 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 int 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); + } 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) throws RocksDBException { + this.cfHandles.clear(); + if (this.readOnly) { + this.db = RocksDB.openReadOnly(this.options, this.dbPath, cfDescriptors, cfHandles); + } else { + this.db = RocksDB.open(this.options, this.dbPath, cfDescriptors, cfHandles); + } + assert cfDescriptors.size() == cfHandles.size(); + + if (this.db == null) { + throw new RocksDBException("open rocksdb null"); + } + try (Env env = this.db.getEnv()) { + env.setBackgroundThreads(8, Priority.LOW); + } + } + + protected abstract boolean postLoad(); + + public synchronized boolean start() { + if (this.loaded) { + return true; + } + if (postLoad()) { + this.loaded = true; + LOGGER.info("RocksDB [{}] starts OK", this.dbPath); + this.closed = false; + return true; + } else { + return false; + } + } + + /** + * Close column family handles except the default column family + */ + protected abstract void preShutdown(); + + public synchronized boolean shutdown() { + try { + if (!this.loaded) { + LOGGER.info("RocksDBStorage is not loaded, shutdown OK. dbPath={}, readOnly={}", this.dbPath, this.readOnly); + 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 matters. + //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(); + } + //4. close db. + if (db != null && !this.readOnly) { + this.db.syncWal(); + } + if (db != null) { + this.db.closeE(); + } + // Close DBOptions after RocksDB instance is closed. + if (this.options != null) { + this.options.close(); + } + //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("RocksDB shutdown OK. {}", this.dbPath); + } catch (Exception e) { + LOGGER.error("RocksDB shutdown failed. {}", this.dbPath, e); + return false; + } + return true; + } + + public void flush(final FlushOptions flushOptions) throws RocksDBException { + flush(flushOptions, this.cfHandles); + } + + public void flush(final FlushOptions flushOptions, List columnFamilyHandles) throws RocksDBException { + if (!this.loaded || this.readOnly || closed) { + return; + } + + try { + if (db != null) { + // For atomic-flush, we have to explicitly specify column family handles + // See https://github.com/rust-rocksdb/rust-rocksdb/pull/793 + // and https://github.com/facebook/rocksdb/blob/8ad4c7efc48d301f5e85467105d7019a49984dc8/include/rocksdb/db.h#L1667 + this.db.flush(flushOptions, columnFamilyHandles); + } + } catch (RocksDBException e) { + scheduleReloadRocksdb(e); + LOGGER.error("flush Failed. {}, {}", this.dbPath, getStatusError(e)); + throw e; + } + } + + public void flushWAL() throws RocksDBException { + this.db.flushWal(true); + } + + 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); + } + + 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 { + // Log Memory Usage + 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("RocksDB Memory Usage: BlockCache: {}, IndexesAndFilterBlock: {}, MemTable: {}, BlocksPinnedByIterator: {}", + blockCacheMemUsage, indexesAndFilterBlockMemUsage, memTableMemUsage, blocksPinnedByIteratorMemUsage); + + // Log file metadata by level + List liveFileMetaDataList = this.getCompactionStatus(); + if (liveFileMetaDataList == null || liveFileMetaDataList.isEmpty()) { + return; + } + Map map = Maps.newHashMap(); + for (LiveFileMetaData metaData : liveFileMetaDataList) { + StringBuilder sb = map.computeIfAbsent(metaData.level(), k -> new StringBuilder(256)); + sb.append(new String(metaData.columnFamilyName(), StandardCharsets.UTF_8)).append(SPACE). + append(metaData.fileName()).append(SPACE). + append("file-size: ").append(metaData.size()).append(SPACE). + append("number-of-entries: ").append(metaData.numEntries()).append(SPACE). + append("file-read-times: ").append(metaData.numReadsSampled()).append(SPACE). + append("deletions: ").append(metaData.numDeletions()).append(SPACE). + append("being-compacted: ").append(metaData.beingCompacted()).append("\n"); + } + map.forEach((key, value) -> logger.info("level: {}\n{}", key, value.toString())); + } catch (Exception ignored) { + } + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/config/ConfigHelper.java b/common/src/main/java/org/apache/rocketmq/common/config/ConfigHelper.java new file mode 100644 index 0000000..e3f6f22 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/config/ConfigHelper.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.common.config; + +import com.google.common.base.Strings; +import java.io.File; +import org.apache.rocketmq.common.UtilAll; +import org.rocksdb.BlockBasedTableConfig; +import org.rocksdb.BloomFilter; +import org.rocksdb.ColumnFamilyOptions; +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 ConfigHelper { + public static ColumnFamilyOptions createConfigColumnFamilyOptions() { + 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(true). + setCacheIndexAndFilterBlocksWithHighPriority(true). + setPinL0FilterAndIndexBlocksInCache(false). + setPinTopLevelIndexAndFilter(true). + setBlockCache(new LRUCache(4 * SizeUnit.MB, 8, false)). + setWholeKeyFiltering(true); + + ColumnFamilyOptions options = new ColumnFamilyOptions(); + return options.setMaxWriteBufferNumber(4). + setWriteBufferSize(64 * 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); + } + + public static DBOptions createConfigDBOptions() { + // Tune 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). + /* + * We use manual flush to achieve desired balance between reliability and performance: + * for metadata that matters, including {topic, subscription}-config changes, each write incurs a + * flush-and-sync to ensure reliability; for {commit, pull}-offset advancements, group-flush are offered for + * every N(configurable, 1024 by default) writes or aging of writes, similar to OS page-cache flush + * mechanism. + */ + setManualWalFlush(true). + // This option takes effect only when we have multiple column families + // https://github.com/facebook/rocksdb/issues/4180 + // setMaxTotalWalSize(1024 * SizeUnit.MB). + setDbWriteBufferSize(128 * SizeUnit.MB). + setBytesPerSync(SizeUnit.MB). + setWalBytesPerSync(SizeUnit.MB). + setCreateIfMissing(true). + setCreateMissingColumnFamilies(true). + setMaxOpenFiles(-1). + setMaxLogFileSize(SizeUnit.GB). + setKeepLogFileNum(5). + setMaxManifestFileSize(SizeUnit.GB). + setAllowConcurrentMemtableWrite(false). + setStatistics(statistics). + setStatsDumpPeriodSec(600). + 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[] rootPaths = new String[] { + System.getProperty("user.home"), + System.getProperty("java.io.tmpdir"), + File.separator + "data" + }; + for (String rootPath : rootPaths) { + // Refer bazel test encyclopedia: https://bazel.build/reference/test-encyclopedia + // Not all directories is available + if (Strings.isNullOrEmpty(rootPath)) { + continue; + } + File rootPathFile = new File(rootPath); + if (!rootPathFile.exists() || !rootPathFile.canWrite()) { + continue; + } + String logDirectory = rootPath + File.separator + "logs" + File.separator + "rocketmqlogs"; + // Create directories recursively. + UtilAll.ensureDirOK(logDirectory); + return logDirectory; + } + throw new RuntimeException("Failed to get log directory"); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/config/ConfigManagerVersion.java b/common/src/main/java/org/apache/rocketmq/common/config/ConfigManagerVersion.java new file mode 100644 index 0000000..0d5dd69 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/config/ConfigManagerVersion.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.config; + +public enum ConfigManagerVersion { + V1("v1"), + V2("v2"), + ; + private final String version; + + ConfigManagerVersion(String version) { + this.version = version; + } + + public String getVersion() { + return version; + } +} 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 0000000..5fd9bab --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/config/ConfigRocksDBStorage.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.common.config; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import org.apache.rocketmq.common.UtilAll; +import org.rocksdb.ColumnFamilyDescriptor; +import org.rocksdb.ColumnFamilyHandle; +import org.rocksdb.ColumnFamilyOptions; +import org.rocksdb.CompressionType; +import org.rocksdb.ReadOptions; +import org.rocksdb.RocksDB; +import org.rocksdb.RocksDBException; +import org.rocksdb.RocksIterator; +import org.rocksdb.WriteBatch; + +public class ConfigRocksDBStorage extends AbstractRocksDBStorage { + public static final byte[] KV_DATA_VERSION_COLUMN_FAMILY_NAME = "kvDataVersion".getBytes(StandardCharsets.UTF_8); + public static final byte[] FORBIDDEN_COLUMN_FAMILY_NAME = "forbidden".getBytes(StandardCharsets.UTF_8); + + protected ColumnFamilyHandle kvDataVersionFamilyHandle; + protected ColumnFamilyHandle forbiddenFamilyHandle; + public static final byte[] KV_DATA_VERSION_KEY = "kvDataVersionKey".getBytes(StandardCharsets.UTF_8); + + + + public ConfigRocksDBStorage(final String dbPath) { + this(dbPath, false); + } + + public ConfigRocksDBStorage(final String dbPath, CompressionType compressionType) { + this(dbPath, false); + this.compressionType = compressionType; + } + + public ConfigRocksDBStorage(final String dbPath, boolean readOnly) { + super(dbPath); + this.readOnly = readOnly; + } + + protected void initOptions() { + this.options = ConfigHelper.createConfigDBOptions(); + super.initOptions(); + } + + @Override + protected boolean postLoad() { + try { + UtilAll.ensureDirOK(this.dbPath); + + initOptions(); + + final List cfDescriptors = new ArrayList<>(); + + ColumnFamilyOptions defaultOptions = ConfigHelper.createConfigColumnFamilyOptions(); + this.cfOptions.add(defaultOptions); + cfDescriptors.add(new ColumnFamilyDescriptor(RocksDB.DEFAULT_COLUMN_FAMILY, defaultOptions)); + cfDescriptors.add(new ColumnFamilyDescriptor(KV_DATA_VERSION_COLUMN_FAMILY_NAME, defaultOptions)); + cfDescriptors.add(new ColumnFamilyDescriptor(FORBIDDEN_COLUMN_FAMILY_NAME, defaultOptions)); + open(cfDescriptors); + + this.defaultCFHandle = cfHandles.get(0); + this.kvDataVersionFamilyHandle = cfHandles.get(1); + this.forbiddenFamilyHandle = cfHandles.get(2); + + } catch (final Exception e) { + AbstractRocksDBStorage.LOGGER.error("postLoad Failed. {}", this.dbPath, e); + return false; + } + return true; + } + + @Override + protected void preShutdown() { + this.kvDataVersionFamilyHandle.close(); + this.forbiddenFamilyHandle.close(); + } + + 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 updateKvDataVersion(final byte[] valueBytes) throws Exception { + put(this.kvDataVersionFamilyHandle, this.ableWalWriteOptions, KV_DATA_VERSION_KEY, KV_DATA_VERSION_KEY.length, valueBytes, valueBytes.length); + } + + public byte[] getKvDataVersion() throws Exception { + return get(this.kvDataVersionFamilyHandle, this.totalOrderReadOptions, KV_DATA_VERSION_KEY); + } + + public void updateForbidden(final byte[] keyBytes, final byte[] valueBytes) throws Exception { + put(this.forbiddenFamilyHandle, this.ableWalWriteOptions, keyBytes, keyBytes.length, valueBytes, valueBytes.length); + } + + public byte[] getForbidden(final byte[] keyBytes) throws Exception { + return get(this.forbiddenFamilyHandle, 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 RocksIterator forbiddenIterator() { + return this.db.newIterator(this.forbiddenFamilyHandle, this.totalOrderReadOptions); + } + + public RocksIterator iterator(ReadOptions readOptions) { + return this.db.newIterator(this.defaultCFHandle, readOptions); + } +} 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 new file mode 100644 index 0000000..08a5885 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/consistenthash/ConsistentHashRouter.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.common.consistenthash; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Collection; +import java.util.Iterator; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * To hash Node objects to a hash ring with a certain amount of virtual node. + * Method routeNode will return a Node instance which the object key should be allocated to according to consistent hash + * algorithm + */ +public class ConsistentHashRouter { + private final SortedMap> ring = new TreeMap<>(); + private final HashFunction hashFunction; + + public ConsistentHashRouter(Collection pNodes, int vNodeCount) { + this(pNodes, vNodeCount, new MD5Hash()); + } + + /** + * @param pNodes collections of physical nodes + * @param vNodeCount amounts of virtual nodes + * @param hashFunction hash Function to hash Node instances + */ + public ConsistentHashRouter(Collection pNodes, int vNodeCount, HashFunction hashFunction) { + if (hashFunction == null) { + throw new NullPointerException("Hash Function is null"); + } + this.hashFunction = hashFunction; + if (pNodes != null) { + for (T pNode : pNodes) { + addNode(pNode, vNodeCount); + } + } + } + + /** + * add physic node to the hash ring with some virtual nodes + * + * @param pNode physical node needs added to hash ring + * @param vNodeCount the number of virtual node of the physical node. Value should be greater than or equals to 0 + */ + public void addNode(T pNode, int vNodeCount) { + if (vNodeCount < 0) + 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); + ring.put(hashFunction.hash(vNode.getKey()), vNode); + } + } + + /** + * remove the physical node from the hash ring + */ + public void removeNode(T pNode) { + Iterator it = ring.keySet().iterator(); + while (it.hasNext()) { + Long key = it.next(); + VirtualNode virtualNode = ring.get(key); + if (virtualNode.isVirtualNodeOf(pNode)) { + it.remove(); + } + } + } + + /** + * with a specified key, route the nearest Node instance in the current hash ring + * + * @param objectKey the object key to find a nearest Node + */ + public T routeNode(String objectKey) { + if (ring.isEmpty()) { + return null; + } + Long hashVal = hashFunction.hash(objectKey); + SortedMap> tailMap = ring.tailMap(hashVal); + Long nodeHashVal = !tailMap.isEmpty() ? tailMap.firstKey() : ring.firstKey(); + return ring.get(nodeHashVal).getPhysicalNode(); + } + + public int getExistingReplicas(T pNode) { + int replicas = 0; + for (VirtualNode vNode : ring.values()) { + if (vNode.isVirtualNodeOf(pNode)) { + replicas++; + } + } + return replicas; + } + + //default hash function + private static class MD5Hash implements HashFunction { + MessageDigest instance; + + public MD5Hash() { + try { + instance = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + } + } + + @Override + public long hash(String key) { + instance.reset(); + instance.update(key.getBytes(StandardCharsets.UTF_8)); + byte[] digest = instance.digest(); + + long h = 0; + for (int i = 0; i < 4; i++) { + h <<= 8; + h |= ((int) digest[i]) & 0xFF; + } + return h; + } + } + +} diff --git a/common/src/main/java/org/apache/rocketmq/common/consistenthash/HashFunction.java b/common/src/main/java/org/apache/rocketmq/common/consistenthash/HashFunction.java new file mode 100644 index 0000000..58fd777 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/consistenthash/HashFunction.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.consistenthash; + +/** + * Hash String to long value + */ +public interface HashFunction { + long hash(String key); +} diff --git a/common/src/main/java/org/apache/rocketmq/common/consistenthash/Node.java b/common/src/main/java/org/apache/rocketmq/common/consistenthash/Node.java new file mode 100644 index 0000000..192600b --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/consistenthash/Node.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.common.consistenthash; + +/** + * Represent a node which should be mapped to a hash ring + */ +public interface Node { + /** + * @return the key which will be used for hash mapping + */ + String getKey(); +} diff --git a/common/src/main/java/org/apache/rocketmq/common/consistenthash/VirtualNode.java b/common/src/main/java/org/apache/rocketmq/common/consistenthash/VirtualNode.java new file mode 100644 index 0000000..c086c01 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/consistenthash/VirtualNode.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.consistenthash; + +public class VirtualNode implements Node { + final T physicalNode; + final int replicaIndex; + + public VirtualNode(T physicalNode, int replicaIndex) { + this.replicaIndex = replicaIndex; + this.physicalNode = physicalNode; + } + + @Override + public String getKey() { + return physicalNode.getKey() + "-" + replicaIndex; + } + + public boolean isVirtualNodeOf(T pNode) { + return physicalNode.getKey().equals(pNode.getKey()); + } + + public T getPhysicalNode() { + return physicalNode; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/constant/CommonConstants.java b/common/src/main/java/org/apache/rocketmq/common/constant/CommonConstants.java new file mode 100644 index 0000000..9fabcb3 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/constant/CommonConstants.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.constant; + +public class CommonConstants { + + public static final String COLON = ":"; + + public static final String ASTERISK = "*"; + + public static final String COMMA = ","; + + public static final String EQUAL = "="; + + public static final String SLASH = "/"; + + public static final String SPACE = " "; + + public static final String HYPHEN = "-"; + + public static final String POUND = "#"; +} 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 0000000..b7091fa --- /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 new file mode 100644 index 0000000..8d2b344 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/constant/DBMsgConstants.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 DBMsgConstants { + 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 0000000..e23a5f5 --- /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/GrpcConstants.java b/common/src/main/java/org/apache/rocketmq/common/constant/GrpcConstants.java new file mode 100644 index 0000000..96293e7 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/constant/GrpcConstants.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.constant; + +import io.grpc.Context; +import io.grpc.Metadata; + +public class GrpcConstants { + 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); + + public static final Metadata.Key CHANNEL_ID + = Metadata.Key.of("x-mq-channel-id", Metadata.ASCII_STRING_MARSHALLER); +} 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 0000000..cd61cea --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/constant/HAProxyConstants.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.constant; + +public class HAProxyConstants { + + public static final String CHANNEL_ID = "channel_id"; + 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 new file mode 100644 index 0000000..9dd84dc --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/constant/LoggerName.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.common.constant; + +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"; + public static final String STORE_ERROR_LOGGER_NAME = "RocketmqStoreError"; + 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"; + + public static final String ROCKETMQ_AUTH_AUDIT_LOGGER_NAME = "RocketmqAuthAudit"; +} 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 new file mode 100644 index 0000000..d9a26a2 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/constant/PermName.java @@ -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. + */ +package org.apache.rocketmq.common.constant; + +public class PermName { + 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 StringBuilder sb = new StringBuilder("---"); + if (isReadable(perm)) { + sb.replace(0, 1, "R"); + } + + if (isWriteable(perm)) { + sb.replace(1, 2, "W"); + } + + if (isInherited(perm)) { + sb.replace(2, 3, "X"); + } + + return sb.toString(); + } + + public static boolean isReadable(final int perm) { + return (perm & PERM_READ) == PERM_READ; + } + + public static boolean isWriteable(final int perm) { + return (perm & PERM_WRITE) == PERM_WRITE; + } + + 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; + } + + public static boolean isAccessible(final int perm) { + return isReadable(perm) || isWriteable(perm); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/consumer/ConsumeFromWhere.java b/common/src/main/java/org/apache/rocketmq/common/consumer/ConsumeFromWhere.java new file mode 100644 index 0000000..a33f465 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/consumer/ConsumeFromWhere.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.common.consumer; + +public enum ConsumeFromWhere { + CONSUME_FROM_LAST_OFFSET, + + @Deprecated + CONSUME_FROM_LAST_OFFSET_AND_FROM_MIN_WHEN_BOOT_FIRST, + @Deprecated + CONSUME_FROM_MIN_OFFSET, + @Deprecated + CONSUME_FROM_MAX_OFFSET, + CONSUME_FROM_FIRST_OFFSET, + CONSUME_FROM_TIMESTAMP, +} 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 0000000..daaaee5 --- /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 0000000..80a1554 --- /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 new file mode 100644 index 0000000..bc37733 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/filter/ExpressionType.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.filter; + +public class ExpressionType { + + /** + *

      + * Keywords: + *
    • {@code AND, OR, NOT, BETWEEN, IN, TRUE, FALSE, IS, NULL}
    • + *
    + *

    + *

      + * Data type: + *
    • Boolean, like: TRUE, FALSE
    • + *
    • String, like: 'abc'
    • + *
    • Decimal, like: 123
    • + *
    • Float number, like: 3.1415
    • + *
    + *

    + *

      + * Grammar: + *
    • {@code AND, OR}
    • + *
    • {@code >, >=, <, <=, =}
    • + *
    • {@code BETWEEN A AND B}, equals to {@code >=A AND <=B}
    • + *
    • {@code NOT BETWEEN A AND B}, equals to {@code >B OR + *
    • {@code IN ('a', 'b')}, equals to {@code ='a' OR ='b'}, this operation only support String type.
    • + *
    • {@code IS NULL}, {@code IS NOT NULL}, check parameter whether is null, or not.
    • + *
    • {@code =TRUE}, {@code =FALSE}, check parameter whether is true, or false.
    • + *
    + *

    + *

    + * Example: + * (a > 10 AND a < 100) OR (b IS NOT NULL AND b=TRUE) + *

    + */ + public static final String SQL92 = "SQL92"; + + /** + * Only support or operation such as + * "tag1 || tag2 || tag3",
    + * If null or * expression,meaning subscribe all. + */ + public static final String TAG = "TAG"; + + public static boolean isTagType(String 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/FilterContext.java b/common/src/main/java/org/apache/rocketmq/common/filter/FilterContext.java new file mode 100644 index 0000000..c04015d --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/filter/FilterContext.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.common.filter; + +public class FilterContext { + private String consumerGroup; + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/filter/MessageFilter.java b/common/src/main/java/org/apache/rocketmq/common/filter/MessageFilter.java new file mode 100644 index 0000000..512c58c --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/filter/MessageFilter.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.filter; + +import org.apache.rocketmq.common.message.MessageExt; + +public interface MessageFilter { + boolean match(final MessageExt msg, final FilterContext context); +} diff --git a/common/src/main/java/org/apache/rocketmq/common/filter/impl/Op.java b/common/src/main/java/org/apache/rocketmq/common/filter/impl/Op.java new file mode 100644 index 0000000..6ad5b66 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/filter/impl/Op.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.filter.impl; + +public abstract class Op { + + private String symbol; + + protected Op(String symbol) { + this.symbol = symbol; + } + + public String getSymbol() { + return symbol; + } + + public String toString() { + return symbol; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/filter/impl/Operand.java b/common/src/main/java/org/apache/rocketmq/common/filter/impl/Operand.java new file mode 100644 index 0000000..2a961df --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/filter/impl/Operand.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.filter.impl; + +public class Operand extends Op { + + public Operand(String symbol) { + super(symbol); + } + +} diff --git a/common/src/main/java/org/apache/rocketmq/common/filter/impl/Operator.java b/common/src/main/java/org/apache/rocketmq/common/filter/impl/Operator.java new file mode 100644 index 0000000..6dd6423 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/filter/impl/Operator.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.filter.impl; + +public class Operator extends Op { + + public static final Operator LEFTPARENTHESIS = new Operator("(", 30, false); + public static final Operator RIGHTPARENTHESIS = new Operator(")", 30, false); + public static final Operator AND = new Operator("&&", 20, true); + public static final Operator OR = new Operator("||", 15, true); + + private int priority; + private boolean compareable; + + private Operator(String symbol, int priority, boolean compareable) { + super(symbol); + this.priority = priority; + this.compareable = compareable; + } + + public static Operator createOperator(String operator) { + if (LEFTPARENTHESIS.getSymbol().equals(operator)) + return LEFTPARENTHESIS; + else if (RIGHTPARENTHESIS.getSymbol().equals(operator)) + return RIGHTPARENTHESIS; + else if (AND.getSymbol().equals(operator)) + return AND; + else if (OR.getSymbol().equals(operator)) + return OR; + else + throw new IllegalArgumentException("unsupport operator " + operator); + } + + public int getPriority() { + return priority; + } + + public boolean isCompareable() { + return compareable; + } + + public int compare(Operator operator) { + if (this.priority > operator.priority) + return 1; + else if (this.priority == operator.priority) + return 0; + else + return -1; + } + + public boolean isSpecifiedOp(String operator) { + return this.getSymbol().equals(operator); + } +} 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 new file mode 100644 index 0000000..e80073f --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/filter/impl/PolishExpr.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.common.filter.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; + +import static org.apache.rocketmq.common.filter.impl.Operator.LEFTPARENTHESIS; +import static org.apache.rocketmq.common.filter.impl.Operator.RIGHTPARENTHESIS; +import static org.apache.rocketmq.common.filter.impl.Operator.createOperator; + +public class PolishExpr { + + public static List reversePolish(String expression) { + return reversePolish(participle(expression)); + } + + /** + * Shunting-yard algorithm
    + * http://en.wikipedia.org/wiki/Shunting_yard_algorithm + * + * @return the compute result of Shunting-yard algorithm + */ + public static List reversePolish(List tokens) { + List segments = new ArrayList<>(); + Stack operatorStack = new Stack<>(); + + for (int i = 0; i < tokens.size(); i++) { + Op token = tokens.get(i); + if (isOperand(token)) { + + segments.add(token); + } else if (isLeftParenthesis(token)) { + + operatorStack.push((Operator) token); + } else if (isRightParenthesis(token)) { + + Operator opNew = null; + while (!operatorStack.empty() && LEFTPARENTHESIS != (opNew = operatorStack.pop())) { + segments.add(opNew); + } + if (null == opNew || LEFTPARENTHESIS != opNew) + throw new IllegalArgumentException("mismatched parentheses"); + } else if (isOperator(token)) { + + Operator opNew = (Operator) token; + if (!operatorStack.empty()) { + Operator opOld = operatorStack.peek(); + if (opOld.isCompareable() && opNew.compare(opOld) != 1) { + segments.add(operatorStack.pop()); + } + } + operatorStack.push(opNew); + } else + throw new IllegalArgumentException("illegal token " + token); + } + + while (!operatorStack.empty()) { + Operator operator = operatorStack.pop(); + if (LEFTPARENTHESIS == operator || RIGHTPARENTHESIS == operator) + throw new IllegalArgumentException("mismatched parentheses " + operator); + segments.add(operator); + } + + return segments; + } + + /** + * @param expression + * @return + * @throws Exception + */ + private static List participle(String expression) { + List segments = new ArrayList<>(); + + int size = expression.length(); + int wordStartIndex = -1; + int wordLen = 0; + Type preType = Type.NULL; + + 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 (Type.OPERATOR == preType || Type.SEPAERATOR == preType || Type.NULL == preType + || Type.PARENTHESIS == preType) { + if (Type.OPERATOR == preType) { + segments.add(createOperator(expression.substring(wordStartIndex, wordStartIndex + + wordLen))); + } + wordStartIndex = i; + wordLen = 0; + } + preType = Type.OPERAND; + wordLen++; + } else if (40 == chValue || 41 == chValue) { + + if (Type.OPERATOR == preType) { + segments.add(createOperator(expression + .substring(wordStartIndex, wordStartIndex + wordLen))); + wordStartIndex = -1; + wordLen = 0; + } else if (Type.OPERAND == preType) { + segments.add(new Operand(expression.substring(wordStartIndex, wordStartIndex + wordLen))); + wordStartIndex = -1; + wordLen = 0; + } + + preType = Type.PARENTHESIS; + segments.add(createOperator((char) chValue + "")); + } else if (38 == chValue || 124 == chValue) { + + if (Type.OPERAND == preType || Type.SEPAERATOR == preType || Type.PARENTHESIS == preType) { + if (Type.OPERAND == preType) { + segments.add(new Operand(expression.substring(wordStartIndex, wordStartIndex + + wordLen))); + } + wordStartIndex = i; + wordLen = 0; + } + preType = Type.OPERATOR; + wordLen++; + } else if (32 == chValue || 9 == chValue) { + + if (Type.OPERATOR == preType) { + segments.add(createOperator(expression + .substring(wordStartIndex, wordStartIndex + wordLen))); + wordStartIndex = -1; + wordLen = 0; + } else if (Type.OPERAND == preType) { + segments.add(new Operand(expression.substring(wordStartIndex, wordStartIndex + wordLen))); + wordStartIndex = -1; + wordLen = 0; + } + preType = Type.SEPAERATOR; + } else { + + throw new IllegalArgumentException("illegal expression, at index " + i + " " + (char) chValue); + } + + } + + if (wordLen > 0) { + segments.add(new Operand(expression.substring(wordStartIndex, wordStartIndex + wordLen))); + } + return segments; + } + + public static boolean isOperand(Op token) { + return token instanceof Operand; + } + + public static boolean isLeftParenthesis(Op token) { + return token instanceof Operator && LEFTPARENTHESIS == (Operator) token; + } + + public static boolean isRightParenthesis(Op token) { + return token instanceof Operator && RIGHTPARENTHESIS == (Operator) token; + } + + public static boolean isOperator(Op token) { + return token instanceof Operator; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/filter/impl/Type.java b/common/src/main/java/org/apache/rocketmq/common/filter/impl/Type.java new file mode 100644 index 0000000..ef55add --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/filter/impl/Type.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.filter.impl; + +public enum Type { + NULL, + OPERAND, + OPERATOR, + PARENTHESIS, + SEPAERATOR; +} diff --git a/common/src/main/java/org/apache/rocketmq/common/future/FutureTaskExt.java b/common/src/main/java/org/apache/rocketmq/common/future/FutureTaskExt.java new file mode 100644 index 0000000..1bd46ea --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/future/FutureTaskExt.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.common.future; + +import java.util.concurrent.Callable; +import java.util.concurrent.FutureTask; + +public class FutureTaskExt extends FutureTask { + private final Runnable runnable; + + public FutureTaskExt(final Callable callable) { + super(callable); + this.runnable = null; + } + + public FutureTaskExt(final Runnable runnable, final V result) { + super(runnable, result); + this.runnable = runnable; + } + + public Runnable getRunnable() { + return runnable; + } +} 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 new file mode 100644 index 0000000..4a6588e --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/help/FAQUrl.java @@ -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. + */ +package org.apache.rocketmq.common.help; + +public class FAQUrl { + + public static final String APPLY_TOPIC_URL = "https://rocketmq.apache.org/docs/bestPractice/06FAQ"; + + 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 = "https://rocketmq.apache.org/docs/bestPractice/06FAQ"; + + public static final String CLIENT_PARAMETER_CHECK_URL = "https://rocketmq.apache.org/docs/bestPractice/06FAQ"; + + public static final String SUBSCRIPTION_GROUP_NOT_EXIST = "https://rocketmq.apache.org/docs/bestPractice/06FAQ"; + + 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 = "https://rocketmq.apache.org/docs/bestPractice/06FAQ"; + + public static final String LOAD_JSON_EXCEPTION = "https://rocketmq.apache.org/docs/bestPractice/06FAQ"; + + public static final String SAME_GROUP_DIFFERENT_TOPIC = "https://rocketmq.apache.org/docs/bestPractice/06FAQ"; + + public static final String MQLIST_NOT_EXIST = "https://rocketmq.apache.org/docs/bestPractice/06FAQ"; + + public static final String UNEXPECTED_EXCEPTION_URL = "https://rocketmq.apache.org/docs/bestPractice/06FAQ"; + + public static final String SEND_MSG_FAILED = "https://rocketmq.apache.org/docs/bestPractice/06FAQ"; + + 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(TIP_STRING_BEGIN.length() + url.length() + TIP_STRING_END.length()); + sb.append(TIP_STRING_BEGIN); + sb.append(url); + sb.append(TIP_STRING_END); + return sb.toString(); + } + + public static String attachDefaultURL(final String errorMessage) { + if (errorMessage != null) { + int index = errorMessage.indexOf(TIP_STRING_BEGIN); + if (-1 == index) { + StringBuilder sb = new StringBuilder(errorMessage.length() + UNEXPECTED_EXCEPTION_URL.length() + MORE_INFORMATION.length() + 1); + sb.append(errorMessage); + sb.append("\n"); + sb.append(MORE_INFORMATION); + sb.append(UNEXPECTED_EXCEPTION_URL); + return sb.toString(); + } + } + + return errorMessage; + } +} 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 new file mode 100644 index 0000000..f57df26 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/hook/FilterCheckHook.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.hook; + +import java.nio.ByteBuffer; + +public interface FilterCheckHook { + String hookName(); + + 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 0000000..19164e3 --- /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 0000000..6995e9a --- /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 new file mode 100644 index 0000000..acd4df9 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/message/Message.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.common.message; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public class Message implements Serializable { + private static final long serialVersionUID = 8445773977080406428L; + + private String topic; + private int flag; + private Map properties; + private byte[] body; + private String transactionId; + + public Message() { + } + + public Message(String topic, byte[] body) { + this(topic, "", "", 0, body, true); + } + + public Message(String topic, String tags, String keys, int flag, byte[] body, boolean waitStoreMsgOK) { + this.topic = topic; + this.flag = flag; + this.body = body; + + if (tags != null && tags.length() > 0) { + this.setTags(tags); + } + + if (keys != null && keys.length() > 0) { + this.setKeys(keys); + } + + this.setWaitStoreMsgOK(waitStoreMsgOK); + } + + public Message(String topic, String tags, byte[] body) { + this(topic, tags, "", 0, body, true); + } + + public Message(String topic, String tags, String keys, byte[] body) { + this(topic, tags, keys, 0, body, true); + } + + public void setKeys(String keys) { + this.putProperty(MessageConst.PROPERTY_KEYS, keys); + } + + void putProperty(final String name, final String value) { + if (null == this.properties) { + this.properties = new HashMap<>(); + } + + this.properties.put(name, value); + } + + void clearProperty(final String name) { + if (null != this.properties) { + this.properties.remove(name); + } + } + + public void putUserProperty(final String name, final String value) { + if (MessageConst.STRING_HASH_SET.contains(name)) { + throw new RuntimeException(String.format( + "The Property<%s> is used by system, input another please", name)); + } + + if (value == null || value.trim().isEmpty() + || name == null || name.trim().isEmpty()) { + throw new IllegalArgumentException( + "The name or value of property can not be null or blank string!" + ); + } + + this.putProperty(name, value); + } + + public String getUserProperty(final String name) { + return this.getProperty(name); + } + + public String getProperty(final String name) { + if (null == this.properties) { + this.properties = new HashMap<>(); + } + + return this.properties.get(name); + } + + public boolean hasProperty(final String name) { + if (null == this.properties) { + return false; + } + return this.properties.containsKey(name); + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getTags() { + return this.getProperty(MessageConst.PROPERTY_TAGS); + } + + public void setTags(String tags) { + this.putProperty(MessageConst.PROPERTY_TAGS, tags); + } + + public String getKeys() { + return this.getProperty(MessageConst.PROPERTY_KEYS); + } + + public void setKeys(Collection keyCollection) { + String keys = String.join(MessageConst.KEY_SEPARATOR, keyCollection); + + this.setKeys(keys); + } + + public int getDelayTimeLevel() { + String t = this.getProperty(MessageConst.PROPERTY_DELAY_TIME_LEVEL); + if (t != null) { + return Integer.parseInt(t); + } + + return 0; + } + + public void setDelayTimeLevel(int level) { + this.putProperty(MessageConst.PROPERTY_DELAY_TIME_LEVEL, String.valueOf(level)); + } + + public boolean isWaitStoreMsgOK() { + String result = this.getProperty(MessageConst.PROPERTY_WAIT_STORE_MSG_OK); + if (null == result) { + return true; + } + + return Boolean.parseBoolean(result); + } + + 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; + } + + public void setFlag(int flag) { + this.flag = flag; + } + + public byte[] getBody() { + return body; + } + + public void setBody(byte[] body) { + this.body = body; + } + + public Map getProperties() { + return properties; + } + + void setProperties(Map properties) { + this.properties = properties; + } + + public String getBuyerId() { + return getProperty(MessageConst.PROPERTY_BUYER_ID); + } + + 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=" + 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 new file mode 100644 index 0000000..62e3bbd --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageAccessor.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.message; + +import java.util.HashMap; +import java.util.Map; + +public class MessageAccessor { + + public static void clearProperty(final Message msg, final String name) { + msg.clearProperty(name); + } + + public static void setProperties(final Message msg, Map properties) { + msg.setProperties(properties); + } + + public static void setTransferFlag(final Message msg, String unit) { + putProperty(msg, MessageConst.PROPERTY_TRANSFER_FLAG, unit); + } + + public static void putProperty(final Message msg, final String name, final String value) { + msg.putProperty(name, value); + } + + public static String getTransferFlag(final Message msg) { + return msg.getProperty(MessageConst.PROPERTY_TRANSFER_FLAG); + } + + public static void setCorrectionFlag(final Message msg, String unit) { + putProperty(msg, MessageConst.PROPERTY_CORRECTION_FLAG, unit); + } + + public static String getCorrectionFlag(final Message msg) { + return msg.getProperty(MessageConst.PROPERTY_CORRECTION_FLAG); + } + + public static void setOriginMessageId(final Message msg, String originMessageId) { + putProperty(msg, MessageConst.PROPERTY_ORIGIN_MESSAGE_ID, originMessageId); + } + + public static String getOriginMessageId(final Message msg) { + return msg.getProperty(MessageConst.PROPERTY_ORIGIN_MESSAGE_ID); + } + + public static void setMQ2Flag(final Message msg, String flag) { + putProperty(msg, MessageConst.PROPERTY_MQ2_FLAG, flag); + } + + public static String getMQ2Flag(final Message msg) { + return msg.getProperty(MessageConst.PROPERTY_MQ2_FLAG); + } + + public static void setReconsumeTime(final Message msg, String reconsumeTimes) { + putProperty(msg, MessageConst.PROPERTY_RECONSUME_TIME, reconsumeTimes); + } + + public static String getReconsumeTime(final Message msg) { + return msg.getProperty(MessageConst.PROPERTY_RECONSUME_TIME); + } + + public static void setMaxReconsumeTimes(final Message msg, String maxReconsumeTimes) { + putProperty(msg, MessageConst.PROPERTY_MAX_RECONSUME_TIMES, maxReconsumeTimes); + } + + public static String getMaxReconsumeTimes(final Message msg) { + return msg.getProperty(MessageConst.PROPERTY_MAX_RECONSUME_TIMES); + } + + public static void setConsumeStartTimeStamp(final Message msg, String propertyConsumeStartTimeStamp) { + putProperty(msg, MessageConst.PROPERTY_CONSUME_START_TIMESTAMP, propertyConsumeStartTimeStamp); + } + + 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 new file mode 100644 index 0000000..30369b8 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageBatch.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.message; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import org.apache.rocketmq.common.MixAll; + +public class MessageBatch extends Message implements Iterable { + + private static final long serialVersionUID = 621335151046335557L; + private final List messages; + + public MessageBatch(List messages) { + this.messages = messages; + } + + public byte[] encode() { + return MessageDecoder.encodeMessages(messages); + } + + public Iterator iterator() { + return messages.iterator(); + } + + public static MessageBatch generateFromList(Collection messages) { + assert messages != null; + assert messages.size() > 0; + List messageList = new ArrayList<>(messages.size()); + Message first = null; + for (Message message : messages) { + if (message.getDelayTimeLevel() > 0) { + 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"); + } + if (first == null) { + first = message; + } else { + if (!first.getTopic().equals(message.getTopic())) { + throw new UnsupportedOperationException("The topic of the messages in one batch should be the same"); + } + if (first.isWaitStoreMsgOK() != message.isWaitStoreMsgOK()) { + throw new UnsupportedOperationException("The waitStoreMsgOK of the messages in one batch should the same"); + } + } + messageList.add(message); + } + MessageBatch messageBatch = new MessageBatch(messageList); + + messageBatch.setTopic(first.getTopic()); + messageBatch.setWaitStoreMsgOK(first.isWaitStoreMsgOK()); + return messageBatch; + } + +} diff --git a/common/src/main/java/org/apache/rocketmq/common/message/MessageClientExt.java b/common/src/main/java/org/apache/rocketmq/common/message/MessageClientExt.java new file mode 100644 index 0000000..808cdbe --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageClientExt.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; + +public class MessageClientExt extends MessageExt { + + public String getOffsetMsgId() { + return super.getMsgId(); + } + + public void setOffsetMsgId(String offsetMsgId) { + super.setMsgId(offsetMsgId); + } + + @Override + public String getMsgId() { + String uniqID = MessageClientIDSetter.getUniqID(this); + if (uniqID == null) { + return this.getOffsetMsgId(); + } else { + return uniqID; + } + } + + public void setMsgId(String msgId) { + //DO NOTHING + //MessageClientIDSetter.setUniqID(this); + } +} 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 new file mode 100644 index 0000000..4ae5ef5 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageClientIDSetter.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 java.util.Calendar; +import java.util.Date; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.rocketmq.common.UtilAll; + +public class MessageClientIDSetter { + + private static final int LEN; + private static final char[] FIX_STRING; + private static final AtomicInteger COUNTER; + private static long startTime; + private static long nextStartTime; + + static { + byte[] ip; + try { + ip = UtilAll.getIP(); + } catch (Exception e) { + ip = createFakeIP(); + } + 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()).toCharArray(); + setStartTime(System.currentTimeMillis()); + COUNTER = new AtomicInteger(0); + } + + private synchronized static void setStartTime(long millis) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(millis); + cal.set(Calendar.DAY_OF_MONTH, 1); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + startTime = cal.getTimeInMillis(); + cal.add(Calendar.MONTH, 1); + nextStartTime = cal.getTimeInMillis(); + } + + 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, ipLength + 2 + 4, 4); + buf.position(0); + long spanMS = buf.getLong(); + Calendar cal = Calendar.getInstance(); + long now = cal.getTimeInMillis(); + cal.set(Calendar.DAY_OF_MONTH, 1); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + long monStartTime = cal.getTimeInMillis(); + if (monStartTime + spanMS >= now) { + cal.add(Calendar.MONTH, -1); + monStartTime = cal.getTimeInMillis(); + } + cal.setTimeInMillis(monStartTime + spanMS); + return cal.getTime(); + } + + public static String getIPStrFromID(String msgID) { + byte[] ipBytes = getIPFromID(msgID); + if (ipBytes.length == 16) { + return UtilAll.ipToIPv6Str(ipBytes); + } else { + return UtilAll.ipToIPv4Str(ipBytes); + } + } + + public static byte[] getIPFromID(String msgID) { + byte[] bytes = UtilAll.string2bytes(msgID); + int ipLength = bytes.length == 28 ? 16 : 4; + byte[] result = new byte[ipLength]; + System.arraycopy(bytes, 0, result, 0, ipLength); + return result; + } + + 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; + } + + 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); + } + 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) { + if (msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX) == null) { + msg.putProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, createUniqID()); + } + } + + public static String getUniqID(final Message msg) { + return msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX); + } + + public static byte[] createFakeIP() { + ByteBuffer bb = ByteBuffer.allocate(8); + bb.putLong(System.currentTimeMillis()); + bb.position(4); + byte[] fakeIP = new byte[4]; + bb.get(fakeIP); + 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 new file mode 100644 index 0000000..24f7bdb --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageConst.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.common.message; + +import java.util.HashSet; + +public class MessageConst { + public static final String PROPERTY_KEYS = "KEYS"; + public static final String PROPERTY_TAGS = "TAGS"; + public static final String PROPERTY_WAIT_STORE_MSG_OK = "WAIT"; + public static final String PROPERTY_DELAY_TIME_LEVEL = "DELAY"; + public static final String PROPERTY_RETRY_TOPIC = "RETRY_TOPIC"; + public static final String PROPERTY_REAL_TOPIC = "REAL_TOPIC"; + public static final String PROPERTY_REAL_QUEUE_ID = "REAL_QID"; + public static final String PROPERTY_TRANSACTION_PREPARED = "TRAN_MSG"; + public static final String PROPERTY_PRODUCER_GROUP = "PGROUP"; + public static final String PROPERTY_MIN_OFFSET = "MIN_OFFSET"; + public static final String PROPERTY_MAX_OFFSET = "MAX_OFFSET"; + public static final String PROPERTY_BUYER_ID = "BUYER_ID"; + public static final String PROPERTY_ORIGIN_MESSAGE_ID = "ORIGIN_MESSAGE_ID"; + public static final String PROPERTY_TRANSFER_FLAG = "TRANSFER_FLAG"; + public static final String PROPERTY_CORRECTION_FLAG = "CORRECTION_FLAG"; + public static final String PROPERTY_MQ2_FLAG = "MQ2_FLAG"; + public static final String PROPERTY_RECONSUME_TIME = "RECONSUME_TIME"; + 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<>(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); + STRING_HASH_SET.add(PROPERTY_MSG_REGION); + STRING_HASH_SET.add(PROPERTY_KEYS); + STRING_HASH_SET.add(PROPERTY_TAGS); + STRING_HASH_SET.add(PROPERTY_WAIT_STORE_MSG_OK); + STRING_HASH_SET.add(PROPERTY_DELAY_TIME_LEVEL); + STRING_HASH_SET.add(PROPERTY_RETRY_TOPIC); + STRING_HASH_SET.add(PROPERTY_REAL_TOPIC); + STRING_HASH_SET.add(PROPERTY_REAL_QUEUE_ID); + STRING_HASH_SET.add(PROPERTY_TRANSACTION_PREPARED); + STRING_HASH_SET.add(PROPERTY_PRODUCER_GROUP); + STRING_HASH_SET.add(PROPERTY_MIN_OFFSET); + STRING_HASH_SET.add(PROPERTY_MAX_OFFSET); + STRING_HASH_SET.add(PROPERTY_BUYER_ID); + STRING_HASH_SET.add(PROPERTY_ORIGIN_MESSAGE_ID); + STRING_HASH_SET.add(PROPERTY_TRANSFER_FLAG); + STRING_HASH_SET.add(PROPERTY_CORRECTION_FLAG); + STRING_HASH_SET.add(PROPERTY_MQ2_FLAG); + STRING_HASH_SET.add(PROPERTY_RECONSUME_TIME); + 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 new file mode 100644 index 0000000..713f940 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageDecoder.java @@ -0,0 +1,793 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 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 = 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 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(); + int msgIDLength = addr.limit() == 8 ? 16 : 28; + input.limit(msgIDLength); + + input.put(addr); + input.putLong(offset); + + return UtilAll.bytes2string(input.array()); + } + + public static String createMessageId(SocketAddress socketAddress, long transactionIdhashCode) { + 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); + byteBuffer.flip(); + return UtilAll.bytes2string(byteBuffer.array()); + } + + public static MessageId decodeMessageId(final String msgId) throws UnknownHostException { + byte[] bytes = UtilAll.string2bytes(msgId); + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + + // 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 + long offset = byteBuffer.getLong(); + + return new MessageId(address, offset); + } + + /** + * Just decode properties from msg buffer. + * + * @param byteBuffer msg commit log buffer. + */ + 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); + return string2messageProperties(propertiesString); + } + return null; + } + + 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(ByteBuffer byteBuffer, final boolean readBody) { + return decode(byteBuffer, readBody, true, true); + } + + public static MessageExt decode(ByteBuffer byteBuffer, final boolean readBody) { + return decode(byteBuffer, readBody, true, false); + } + + public static byte[] encode(MessageExt messageExt, boolean needCompress) throws Exception { + 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; + int storehostAddressLength = (sysFlag & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0 ? 8 : 20; + byte[] newBody = messageExt.getBody(); + if (needCompress && (sysFlag & MessageSysFlag.COMPRESSED_FLAG) == MessageSysFlag.COMPRESSED_FLAG) { + Compressor compressor = CompressorFactory.getCompressor(MessageSysFlag.getCompressionType(sysFlag)); + newBody = compressor.compress(body, 5); + } + int bodyLength = newBody.length; + int storeSize = messageExt.getStoreSize(); + ByteBuffer byteBuffer; + if (storeSize > 0) { + byteBuffer = ByteBuffer.allocate(storeSize); + } 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 + + 8 // 11 STORETIMESTAMP + + storehostAddressLength // 12 STOREHOSTADDRESS + + 4 // 13 RECONSUMETIMES + + 8 // 14 Prepared Transaction Offset + + 4 + bodyLength // 14 BODY + + 1 + topicLen // 15 TOPIC + + 2 + propertiesLength // 16 propertiesLength + + 0; + 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 STORETIMESTAMP + long storeTimestamp = messageExt.getStoreTimestamp(); + byteBuffer.putLong(storeTimestamp); + + // 12 STOREHOST + InetSocketAddress serverHost = (InetSocketAddress) messageExt.getStoreHost(); + byteBuffer.put(serverHost.getAddress().getAddress()); + byteBuffer.putInt(serverHost.getPort()); + + // 13 RECONSUMETIMES + int reconsumeTimes = messageExt.getReconsumeTimes(); + byteBuffer.putInt(reconsumeTimes); + + // 14 Prepared Transaction Offset + long preparedTransactionOffset = messageExt.getPreparedTransactionOffset(); + byteBuffer.putLong(preparedTransactionOffset); + + // 15 BODY + byteBuffer.putInt(bodyLength); + byteBuffer.put(newBody); + + // 16 TOPIC + byteBuffer.put(topicLen); + byteBuffer.put(topics); + + // 17 properties + byteBuffer.putShort(propertiesLength); + byteBuffer.put(propertiesBytes); + + 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( + 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; + if (isClient) { + msgExt = new MessageClientExt(); + } else { + msgExt = new MessageExt(); + } + + // 1 TOTALSIZE + int storeSize = byteBuffer.getInt(); + msgExt.setStoreSize(storeSize); + + // 2 MAGICCODE + int magicCode = byteBuffer.getInt(); + MessageVersion version = MessageVersion.valueOfMagicCode(magicCode); + + // 3 BODYCRC + int bodyCRC = byteBuffer.getInt(); + msgExt.setBodyCRC(bodyCRC); + + // 4 QUEUEID + int queueId = byteBuffer.getInt(); + msgExt.setQueueId(queueId); + + // 5 FLAG + int flag = byteBuffer.getInt(); + msgExt.setFlag(flag); + + // 6 QUEUEOFFSET + long queueOffset = byteBuffer.getLong(); + msgExt.setQueueOffset(queueOffset); + + // 7 PHYSICALOFFSET + long physicOffset = byteBuffer.getLong(); + msgExt.setCommitLogOffset(physicOffset); + + // 8 SYSFLAG + int sysFlag = byteBuffer.getInt(); + msgExt.setSysFlag(sysFlag); + + // 9 BORNTIMESTAMP + long bornTimeStamp = byteBuffer.getLong(); + msgExt.setBornTimestamp(bornTimeStamp); + + // 10 BORNHOST + 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)); + + // 11 STORETIMESTAMP + long storeTimestamp = byteBuffer.getLong(); + msgExt.setStoreTimestamp(storeTimestamp); + + // 12 STOREHOST + 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)); + + // 13 RECONSUMETIMES + int reconsumeTimes = byteBuffer.getInt(); + msgExt.setReconsumeTimes(reconsumeTimes); + + // 14 Prepared Transaction Offset + long preparedTransactionOffset = byteBuffer.getLong(); + msgExt.setPreparedTransactionOffset(preparedTransactionOffset); + + // 15 BODY + int bodyLen = byteBuffer.getInt(); + if (bodyLen > 0) { + if (readBody) { + 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!"); + } + } + + // inflate body + if (deCompressBody && (sysFlag & MessageSysFlag.COMPRESSED_FLAG) == MessageSysFlag.COMPRESSED_FLAG) { + Compressor compressor = CompressorFactory.getCompressor(MessageSysFlag.getCompressionType(sysFlag)); + body = compressor.decompress(body); + sysFlag &= ~MessageSysFlag.COMPRESSED_FLAG; + } + + msgExt.setBody(body); + msgExt.setSysFlag(sysFlag); + } else { + byteBuffer.position(byteBuffer.position() + bodyLen); + } + } + + // 16 TOPIC + int topicLen = version.getTopicLength(byteBuffer); + byte[] topic = new byte[topicLen]; + byteBuffer.get(topic); + msgExt.setTopic(new String(topic, CHARSET_UTF8)); + + // 17 properties + short propertiesLength = byteBuffer.getShort(); + if (propertiesLength > 0) { + byte[] properties = new byte[propertiesLength]; + byteBuffer.get(properties); + String propertiesString = new String(properties, CHARSET_UTF8); + if (!isSetPropertiesString) { + Map map = string2messageProperties(propertiesString); + msgExt.setProperties(map); + } else { + Map map = string2messageProperties(propertiesString); + map.put("propertiesString", propertiesString); + msgExt.setProperties(map); + } + } + + int msgIDLength = storehostIPLength + 4 + 8; + ByteBuffer byteBufferMsgId = ByteBuffer.allocate(msgIDLength); + String msgId = createMessageId(byteBufferMsgId, msgExt.getStoreHostBytes(), msgExt.getCommitLogOffset()); + msgExt.setMsgId(msgId); + + if (isClient) { + ((MessageClientExt) msgExt).setOffsetMsgId(msgId); + } + + return msgExt; + } catch (Exception e) { + byteBuffer.position(byteBuffer.limit()); + } + + return null; + } + + public static List decodes(ByteBuffer byteBuffer) { + return decodes(byteBuffer, true); + } + + 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) { + msgExts.add(msgExt); + } else { + break; + } + } + return msgExts; + } + + public static String messageProperties2String(Map properties) { + 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<>(128); + if (properties != null) { + 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; + } + } + + return map; + } + + public static byte[] encodeMessage(Message message) { + //only need flag, body, properties + byte[] body = message.getBody(); + int bodyLen = body.length; + String properties = messageProperties2String(message.getProperties()); + byte[] propertiesBytes = properties.getBytes(CHARSET_UTF8); + //note properties length must not more than Short.MAX + short propertiesLength = (short) propertiesBytes.length; + int storeSize = 4 // 1 TOTALSIZE + + 4 // 2 MAGICCOD + + 4 // 3 BODYCRC + + 4 // 4 FLAG + + 4 + bodyLen // 4 BODY + + 2 + propertiesLength; + ByteBuffer byteBuffer = ByteBuffer.allocate(storeSize); + // 1 TOTALSIZE + byteBuffer.putInt(storeSize); + + // 2 MAGICCODE + byteBuffer.putInt(0); + + // 3 BODYCRC + byteBuffer.putInt(0); + + // 4 FLAG + int flag = message.getFlag(); + byteBuffer.putInt(flag); + + // 5 BODY + byteBuffer.putInt(bodyLen); + byteBuffer.put(body); + + // 6 properties + byteBuffer.putShort(propertiesLength); + byteBuffer.put(propertiesBytes); + + return byteBuffer.array(); + } + + public static Message decodeMessage(ByteBuffer byteBuffer) throws Exception { + Message message = new Message(); + + // 1 TOTALSIZE + byteBuffer.getInt(); + + // 2 MAGICCODE + byteBuffer.getInt(); + + // 3 BODYCRC + byteBuffer.getInt(); + + // 4 FLAG + int flag = byteBuffer.getInt(); + message.setFlag(flag); + + // 5 BODY + int bodyLen = byteBuffer.getInt(); + byte[] body = new byte[bodyLen]; + byteBuffer.get(body); + message.setBody(body); + + // 6 properties + short propertiesLen = byteBuffer.getShort(); + byte[] propertiesBytes = new byte[propertiesLen]; + byteBuffer.get(propertiesBytes); + message.setProperties(string2messageProperties(new String(propertiesBytes, CHARSET_UTF8))); + + return message; + } + + public static byte[] encodeMessages(List messages) { + //TO DO refactor, accumulate in one buffer, avoid copies + List encodedMessages = new ArrayList<>(messages.size()); + int allSize = 0; + for (Message message : messages) { + byte[] tmp = encodeMessage(message); + encodedMessages.add(tmp); + allSize += tmp.length; + } + byte[] allBytes = new byte[allSize]; + int pos = 0; + for (byte[] bytes : encodedMessages) { + System.arraycopy(bytes, 0, allBytes, pos, bytes.length); + pos += bytes.length; + } + return allBytes; + } + + public static List decodeMessages(ByteBuffer byteBuffer) throws Exception { + //TO DO add a callback for processing, avoid creating lists + 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 new file mode 100644 index 0000000..5905d28 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageExt.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.common.message; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import org.apache.rocketmq.common.TopicFilterType; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; + +public class MessageExt extends Message { + private static final long serialVersionUID = 5720810158625748049L; + + private String brokerName; + + private int queueId; + + private int storeSize; + + private long queueOffset; + private int sysFlag; + private long bornTimestamp; + private SocketAddress bornHost; + + private long storeTimestamp; + private SocketAddress storeHost; + private String msgId; + private long commitLogOffset; + private int bodyCRC; + private int reconsumeTimes; + + private long preparedTransactionOffset; + + public MessageExt() { + } + + public MessageExt(int queueId, long bornTimestamp, SocketAddress bornHost, long storeTimestamp, + SocketAddress storeHost, String msgId) { + this.queueId = queueId; + this.bornTimestamp = bornTimestamp; + this.bornHost = bornHost; + this.storeTimestamp = storeTimestamp; + this.storeHost = storeHost; + this.msgId = msgId; + } + + public static TopicFilterType parseTopicFilterType(final int sysFlag) { + if ((sysFlag & MessageSysFlag.MULTI_TAGS_FLAG) == MessageSysFlag.MULTI_TAGS_FLAG) { + return TopicFilterType.MULTI_TAG; + } + + return TopicFilterType.SINGLE_TAG; + } + + public static ByteBuffer socketAddress2ByteBuffer(final SocketAddress socketAddress, final ByteBuffer byteBuffer) { + InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress; + 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) { + 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); + } + + public ByteBuffer getBornHostBytes() { + return socketAddress2ByteBuffer(this.bornHost); + } + + public ByteBuffer getBornHostBytes(ByteBuffer byteBuffer) { + return socketAddress2ByteBuffer(this.bornHost, byteBuffer); + } + + public ByteBuffer getStoreHostBytes() { + return socketAddress2ByteBuffer(this.storeHost); + } + + 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; + } + + public void setQueueId(int queueId) { + this.queueId = queueId; + } + + public long getBornTimestamp() { + return bornTimestamp; + } + + public void setBornTimestamp(long bornTimestamp) { + this.bornTimestamp = bornTimestamp; + } + + public SocketAddress getBornHost() { + return bornHost; + } + + public void setBornHost(SocketAddress bornHost) { + this.bornHost = bornHost; + } + + public String getBornHostString() { + if (null != this.bornHost) { + InetAddress inetAddress = ((InetSocketAddress) this.bornHost).getAddress(); + + return null != inetAddress ? inetAddress.getHostAddress() : null; + } + + return null; + } + + public String getBornHostNameString() { + 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; + } + + public long getStoreTimestamp() { + return storeTimestamp; + } + + public void setStoreTimestamp(long storeTimestamp) { + this.storeTimestamp = storeTimestamp; + } + + public SocketAddress getStoreHost() { + return storeHost; + } + + public void setStoreHost(SocketAddress storeHost) { + this.storeHost = storeHost; + } + + public String getMsgId() { + return msgId; + } + + public void setMsgId(String msgId) { + this.msgId = msgId; + } + + public int getSysFlag() { + return sysFlag; + } + + 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; + } + + public void setBodyCRC(int bodyCRC) { + this.bodyCRC = bodyCRC; + } + + public long getQueueOffset() { + return queueOffset; + } + + public void setQueueOffset(long queueOffset) { + this.queueOffset = queueOffset; + } + + public long getCommitLogOffset() { + return commitLogOffset; + } + + public void setCommitLogOffset(long physicOffset) { + this.commitLogOffset = physicOffset; + } + + public int getStoreSize() { + return storeSize; + } + + public void setStoreSize(int storeSize) { + this.storeSize = storeSize; + } + + public int getReconsumeTimes() { + return reconsumeTimes; + } + + public void setReconsumeTimes(int reconsumeTimes) { + this.reconsumeTimes = reconsumeTimes; + } + + public long getPreparedTransactionOffset() { + return preparedTransactionOffset; + } + + 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 [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=" + + reconsumeTimes + ", preparedTransactionOffset=" + preparedTransactionOffset + + ", toString()=" + super.toString() + "]"; + } +} 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 new file mode 100644 index 0000000..42f98e4 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageExtBatch.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.message; + +import java.nio.ByteBuffer; + +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() { + return encodedBuff; + } + + public void setEncodedBuff(ByteBuffer encodedBuff) { + this.encodedBuff = encodedBuff; + } +} 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 0000000..e4f02e2 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageExtBrokerInner.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.message; + +import com.google.common.base.Strings; +import java.nio.ByteBuffer; + +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.MixAll; +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 (Strings.isNullOrEmpty(tags)) { 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; + } + + public boolean needDispatchLMQ() { + return StringUtils.isNoneBlank(getProperty(MessageConst.PROPERTY_INNER_MULTI_DISPATCH)) + && MixAll.topicAllowsLMQ(getTopic()); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/message/MessageId.java b/common/src/main/java/org/apache/rocketmq/common/message/MessageId.java new file mode 100644 index 0000000..fc8e303 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageId.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.message; + +import java.net.SocketAddress; + +public class MessageId { + private SocketAddress address; + private long offset; + + public MessageId(SocketAddress address, long offset) { + this.address = address; + this.offset = offset; + } + + public SocketAddress getAddress() { + return address; + } + + public void setAddress(SocketAddress address) { + this.address = address; + } + + public long getOffset() { + return offset; + } + + public void setOffset(long offset) { + this.offset = offset; + } +} 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 new file mode 100644 index 0000000..7926b73 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageQueue.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.common.message; + +import java.io.Serializable; + +public class MessageQueue implements Comparable, Serializable { + private static final long serialVersionUID = 6191200464116433425L; + private String topic; + private String brokerName; + private int queueId; + + 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; + this.queueId = queueId; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public int getQueueId() { + return queueId; + } + + public void setQueueId(int queueId) { + this.queueId = queueId; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((brokerName == null) ? 0 : brokerName.hashCode()); + result = prime * result + queueId; + 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; + MessageQueue other = (MessageQueue) obj; + if (brokerName == null) { + if (other.brokerName != null) + return false; + } else if (!brokerName.equals(other.brokerName)) + return false; + if (queueId != other.queueId) + return false; + if (topic == null) { + if (other.topic != null) + return false; + } else if (!topic.equals(other.topic)) + return false; + return true; + } + + @Override + public String toString() { + return "MessageQueue [topic=" + topic + ", brokerName=" + brokerName + ", queueId=" + queueId + "]"; + } + + @Override + public int compareTo(MessageQueue o) { + { + int result = this.topic.compareTo(o.topic); + if (result != 0) { + return result; + } + } + + { + int result = this.brokerName.compareTo(o.brokerName); + if (result != 0) { + return result; + } + } + + return this.queueId - o.queueId; + } +} 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 0000000..fcd9f58 --- /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/MessageQueueForC.java b/common/src/main/java/org/apache/rocketmq/common/message/MessageQueueForC.java new file mode 100644 index 0000000..30cc966 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageQueueForC.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.common.message; + +import java.io.Serializable; + +public class MessageQueueForC implements Comparable, Serializable { + + private static final long serialVersionUID = 5320967846569962104L; + private String topic; + private String brokerName; + private int queueId; + private long offset; + + public MessageQueueForC(String topic, String brokerName, int queueId, long offset) { + this.topic = topic; + this.brokerName = brokerName; + this.queueId = queueId; + this.offset = offset; + } + + @Override + public int compareTo(MessageQueueForC o) { + int result = this.topic.compareTo(o.topic); + if (result != 0) { + return result; + } + result = this.brokerName.compareTo(o.brokerName); + if (result != 0) { + return result; + } + result = this.queueId - o.queueId; + if (result != 0) { + return result; + } + if ((this.offset - o.offset) > 0) { + return 1; + } else if ((this.offset - o.offset) == 0) { + return 0; + } else { + return -1; + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((brokerName == null) ? 0 : brokerName.hashCode()); + result = prime * result + queueId; + 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; + MessageQueueForC other = (MessageQueueForC) obj; + if (brokerName == null) { + if (other.brokerName != null) + return false; + } else if (!brokerName.equals(other.brokerName)) + return false; + if (queueId != other.queueId) + return false; + if (topic == null) { + if (other.topic != null) + return false; + } else if (!topic.equals(other.topic)) + return false; + + if (offset != other.offset) { + return false; + } + return true; + } + + @Override + public String toString() { + return "MessageQueueForC [topic=" + topic + ", brokerName=" + brokerName + ", queueId=" + queueId + + ", offset=" + offset + "]"; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public int getQueueId() { + return queueId; + } + + public void setQueueId(int queueId) { + this.queueId = queueId; + } + + public long getOffset() { + return offset; + } + + public void setOffset(long offset) { + this.offset = offset; + } +} 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 0000000..35a166a --- /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 new file mode 100644 index 0000000..5ffd2f6 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageType.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.message; + +public enum MessageType { + 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 0000000..bb1c2e8 --- /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 0000000..5f065b4 --- /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 0000000..a281216 --- /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 0000000..e967c63 --- /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 0000000..3e8be19 --- /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 0000000..899ac14 --- /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 0000000..091fa72 --- /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 0000000..0636e30 --- /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) { + StringBuilder url = new StringBuilder(this.wsAddr); + try { + if (null != para && para.size() > 0) { + if (!UtilAll.isBlank(this.unitName)) { + url.append("-").append(this.unitName).append("?nofix=1&"); + } + else { + url.append("?"); + } + for (Map.Entry entry : this.para.entrySet()) { + url.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); + } + url = new StringBuilder(url.substring(0, url.length() - 1)); + } + else { + if (!UtilAll.isBlank(this.unitName)) { + url.append("-").append(this.unitName).append("?nofix=1"); + } + } + + HttpTinyClient.HttpResult result = HttpTinyClient.httpGet(url.toString(), 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 0000000..67ce2b8 --- /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 new file mode 100644 index 0000000..d1cdc76 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/namesrv/NamesrvConfig.java @@ -0,0 +1,275 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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: NamesrvConfig.java 1839 2013-05-16 02:12:02Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.common.namesrv; + +import java.io.File; +import org.apache.rocketmq.common.MixAll; + +public class NamesrvConfig { + + 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"; + private String configStorePath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "namesrv.properties"; + 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; + } + + public void setOrderMessageEnable(boolean orderMessageEnable) { + this.orderMessageEnable = orderMessageEnable; + } + + public String getRocketmqHome() { + return rocketmqHome; + } + + public void setRocketmqHome(String rocketmqHome) { + this.rocketmqHome = rocketmqHome; + } + + public String getKvConfigPath() { + return kvConfigPath; + } + + public void setKvConfigPath(String kvConfigPath) { + this.kvConfigPath = kvConfigPath; + } + + public String getProductEnvName() { + return productEnvName; + } + + public void setProductEnvName(String productEnvName) { + this.productEnvName = productEnvName; + } + + public boolean isClusterTest() { + return clusterTest; + } + + public void setClusterTest(boolean clusterTest) { + this.clusterTest = clusterTest; + } + + public String getConfigStorePath() { + return configStorePath; + } + + 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/NamesrvUtil.java b/common/src/main/java/org/apache/rocketmq/common/namesrv/NamesrvUtil.java new file mode 100644 index 0000000..90b1093 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/namesrv/NamesrvUtil.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.namesrv; + +public class NamesrvUtil { + public static final String NAMESPACE_ORDER_TOPIC_CONFIG = "ORDER_TOPIC_CONFIG"; +} 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 new file mode 100644 index 0000000..3ca1828 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/namesrv/TopAddressing.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.common.namesrv; + + +public interface TopAddressing { + + String fetchNSAddr(); + + void registerChangeCallBack(NameServerUpdateCallback changeCallBack); +} diff --git a/common/src/main/java/org/apache/rocketmq/common/producer/RecallMessageHandle.java b/common/src/main/java/org/apache/rocketmq/common/producer/RecallMessageHandle.java new file mode 100644 index 0000000..b00b15b --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/producer/RecallMessageHandle.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.common.producer; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.lang3.StringUtils; + +import java.util.Base64; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * handle to recall a message, only support delay message for now + * v1 pattern like this: + * version topic brokerName timestamp messageId + * use Base64 to encode it + */ +public class RecallMessageHandle { + private static final String SEPARATOR = " "; + private static final String VERSION_1 = "v1"; + + public static class HandleV1 extends RecallMessageHandle { + private String version; + private String topic; + private String brokerName; + private String timestampStr; + private String messageId; // id of unique key + + public HandleV1(String topic, String brokerName, String timestamp, String messageId) { + this.version = VERSION_1; + this.topic = topic; + this.brokerName = brokerName; + this.timestampStr = timestamp; + this.messageId = messageId; + } + + // no param check + public static String buildHandle(String topic, String brokerName, String timestampStr, String messageId) { + String rawString = String.join(SEPARATOR, VERSION_1, topic, brokerName, timestampStr, messageId); + return Base64.getUrlEncoder().encodeToString(rawString.getBytes(UTF_8)); + } + + public String getTopic() { + return topic; + } + + public String getBrokerName() { + return brokerName; + } + + public String getTimestampStr() { + return timestampStr; + } + + public String getMessageId() { + return messageId; + } + + public String getVersion() { + return version; + } + } + + public static RecallMessageHandle decodeHandle(String handle) throws DecoderException { + if (StringUtils.isEmpty(handle)) { + throw new DecoderException("recall handle is invalid"); + } + String rawString; + try { + rawString = + new String(Base64.getUrlDecoder().decode(handle.getBytes(UTF_8)), UTF_8); + } catch (IllegalArgumentException e) { + throw new DecoderException("recall handle is invalid"); + } + String[] items = rawString.split(SEPARATOR); + if (!VERSION_1.equals(items[0]) || items.length < 5) { + throw new DecoderException("recall handle is invalid"); + } + return new HandleV1(items[1], items[2], items[3], items[4]); + } +} 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 new file mode 100644 index 0000000..1df2f96 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/queue/ConcurrentTreeMap.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.common.queue; + +import java.util.Comparator; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.locks.ReentrantLock; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +/** + * thread safe + */ +public class ConcurrentTreeMap { + private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + private final ReentrantLock lock; + private TreeMap tree; + private RoundQueue roundQueue; + + public ConcurrentTreeMap(int capacity, Comparator comparator) { + tree = new TreeMap<>(comparator); + roundQueue = new RoundQueue<>(capacity); + lock = new ReentrantLock(true); + } + + public Map.Entry pollFirstEntry() { + lock.lock(); + try { + return tree.pollFirstEntry(); + } finally { + lock.unlock(); + } + } + + public V putIfAbsentAndRetExsit(K key, V value) { + lock.lock(); + try { + if (roundQueue.put(key)) { + V exsit = tree.get(key); + if (null == exsit) { + tree.put(key, value); + exsit = value; + } + log.warn("putIfAbsentAndRetExsit success. " + key); + return exsit; + } else { + V exsit = tree.get(key); + return exsit; + } + } finally { + lock.unlock(); + } + } + +} 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 new file mode 100644 index 0000000..8fc5f68 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/queue/RoundQueue.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.queue; + +import java.util.LinkedList; +import java.util.Queue; + +/** + * not thread safe + */ +public class RoundQueue { + + private Queue queue; + private int capacity; + + public RoundQueue(int capacity) { + this.capacity = capacity; + queue = new LinkedList<>(); + } + + public boolean put(E e) { + boolean ok = false; + if (!queue.contains(e)) { + if (queue.size() >= capacity) { + queue.poll(); + } + queue.add(e); + ok = true; + } + + return ok; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/resource/ResourcePattern.java b/common/src/main/java/org/apache/rocketmq/common/resource/ResourcePattern.java new file mode 100644 index 0000000..6fac4b7 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/resource/ResourcePattern.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.resource; + +import com.alibaba.fastjson2.annotation.JSONField; + +public enum ResourcePattern { + + ANY((byte) 1, "ANY"), + + LITERAL((byte) 2, "LITERAL"), + + PREFIXED((byte) 3, "PREFIXED"); + + @JSONField(value = true) + private final byte code; + private final String name; + + ResourcePattern(byte code, String name) { + this.code = code; + this.name = name; + } + + public byte getCode() { + return code; + } + + public String getName() { + return name; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/resource/ResourceType.java b/common/src/main/java/org/apache/rocketmq/common/resource/ResourceType.java new file mode 100644 index 0000000..479b8e5 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/resource/ResourceType.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.common.resource; + +import com.alibaba.fastjson2.annotation.JSONField; +import org.apache.commons.lang3.StringUtils; + +public enum ResourceType { + + UNKNOWN((byte) 0, "Unknown"), + + ANY((byte) 1, "Any"), + + CLUSTER((byte) 2, "Cluster"), + + NAMESPACE((byte) 3, "Namespace"), + + TOPIC((byte) 4, "Topic"), + + GROUP((byte) 5, "Group"); + + @JSONField(value = true) + private final byte code; + private final String name; + + ResourceType(byte code, String name) { + this.code = code; + this.name = name; + } + + public static ResourceType getByName(String name) { + for (ResourceType resourceType : ResourceType.values()) { + if (StringUtils.equalsIgnoreCase(resourceType.getName(), name)) { + return resourceType; + } + } + return null; + } + + public byte getCode() { + return code; + } + + public String getName() { + return name; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/resource/RocketMQResource.java b/common/src/main/java/org/apache/rocketmq/common/resource/RocketMQResource.java new file mode 100644 index 0000000..f3df670 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/resource/RocketMQResource.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.resource; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface RocketMQResource { + + ResourceType value(); + + String splitter() default ""; +} diff --git a/common/src/main/java/org/apache/rocketmq/common/running/RunningStats.java b/common/src/main/java/org/apache/rocketmq/common/running/RunningStats.java new file mode 100644 index 0000000..5a11bb2 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/running/RunningStats.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.common.running; + +public enum RunningStats { + commitLogMaxOffset, + commitLogMinOffset, + commitLogDiskRatio, + consumeQueueDiskRatio, + scheduleMessageOffset, +} 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 0000000..aed04dc --- /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 0000000..1fcf0b8 --- /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 0000000..0af55e0 --- /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 0000000..970bff9 --- /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 0000000..3ec295f --- /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 briefItemName: " + 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 0000000..23633dc --- /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 0000000..f3a9bdb --- /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 0000000..1f46590 --- /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 0000000..e199847 --- /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 interceptor = item.getInterceptor(); + String interceptorStr = formatInterceptor(interceptor); + if (interceptor != null) { + interceptor.reset(); + } + + StatisticsItemSampleBrief brief = getSampleBrief(item.getStatKind(), item.getStatObject()); + if (brief != null && (!increment.allZeros() || printZeroLine())) { + printer.print(name, increment, interceptorStr, brief.toString()); + } + + setItemSnapshot(lastItemSnapshots, snapshot); + + if (brief != null) { + brief.reset(); + } + } + }, getInitialDelay(), interval, TimeUnit.MILLISECONDS); + addFuture(item, future); + + // sample every TPS_INTERVAL + 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 0000000..799c02c --- /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 0000000..3b16d00 --- /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 0000000..27bee19 --- /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 0000000..8d6bdb7 --- /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 new file mode 100644 index 0000000..559bb77 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/stats/MomentStatsItem.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.common.stats; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.logging.org.slf4j.Logger; + +public class MomentStatsItem { + + private final AtomicLong value = new AtomicLong(0); + + private final String statsName; + private final String statsKey; + private final ScheduledExecutorService scheduledExecutorService; + private final Logger log; + private long lastUpdateTimestamp = System.currentTimeMillis(); + + public MomentStatsItem(String statsName, String statsKey, + ScheduledExecutorService scheduledExecutorService, Logger log) { + this.statsName = statsName; + this.statsKey = statsKey; + this.scheduledExecutorService = scheduledExecutorService; + this.log = log; + } + + public void init() { + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + printAtMinutes(); + + MomentStatsItem.this.value.set(0); + } catch (Throwable e) { + } + } + }, Math.abs(UtilAll.computeNextMinutesTimeMillis() - System.currentTimeMillis()), 1000 * 60 * 5, TimeUnit.MILLISECONDS); + } + + public void printAtMinutes() { + log.info("[{}] [{}] Stats Every 5 Minutes, Value: {}", + this.statsName, + this.statsKey, + this.value.get()); + } + + public AtomicLong getValue() { + return value; + } + + public String getStatsKey() { + return statsKey; + } + + public String getStatsName() { + return statsName; + } + + public long getLastUpdateTimestamp() { + return lastUpdateTimestamp; + } + + public void setLastUpdateTimestamp(long lastUpdateTimestamp) { + this.lastUpdateTimestamp = lastUpdateTimestamp; + } +} 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 new file mode 100644 index 0000000..fd65351 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/stats/MomentStatsItemSet.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.common.stats; + +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +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 MomentStatsItemSet { + private static final Logger COMMERCIAL_LOG = LoggerFactory.getLogger(LoggerName.COMMERCIAL_LOGGER_NAME); + private final ConcurrentMap statsItemTable = + new ConcurrentHashMap<>(128); + private final String statsName; + private final ScheduledExecutorService scheduledExecutorService; + private final Logger log; + + public MomentStatsItemSet(String statsName, ScheduledExecutorService scheduledExecutorService, Logger log) { + this.statsName = statsName; + this.scheduledExecutorService = scheduledExecutorService; + this.log = log; + this.init(); + } + + public ConcurrentMap getStatsItemTable() { + return statsItemTable; + } + + public String getStatsName() { + return statsName; + } + + public void init() { + + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + printAtMinutes(); + } catch (Throwable ignored) { + } + } + }, Math.abs(UtilAll.computeNextMinutesTimeMillis() - System.currentTimeMillis()), 1000 * 60 * 5, TimeUnit.MILLISECONDS); + } + + private void printAtMinutes() { + Iterator> it = this.statsItemTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + next.getValue().printAtMinutes(); + } + } + + public void setValue(final String statsKey, final int value) { + MomentStatsItem statsItem = this.getAndCreateStatsItem(statsKey); + statsItem.getValue().set(value); + statsItem.setLastUpdateTimestamp(System.currentTimeMillis()); + } + + public void setValue(final String statsKey, final long value) { + MomentStatsItem statsItem = this.getAndCreateStatsItem(statsKey); + statsItem.getValue().set(value); + statsItem.setLastUpdateTimestamp(System.currentTimeMillis()); + } + + 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.putIfAbsent(statsKey, statsItem); + + if (null != prev) { + statsItem = prev; + // statsItem.init(); + } + } + + return statsItem; + } + + public void cleanResource(int maxStatsIdleTimeInMinutes) { + COMMERCIAL_LOG.info("CleanStatisticItem: kind:{}, size:{}", statsName, this.statsItemTable.size()); + Iterator> it = this.statsItemTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + MomentStatsItem statsItem = next.getValue(); + if (System.currentTimeMillis() - statsItem.getLastUpdateTimestamp() > maxStatsIdleTimeInMinutes * 60 * 1000L) { + it.remove(); + COMMERCIAL_LOG.info("CleanStatisticItem: removeKind:{}, removeKey:{}", statsName, statsItem.getStatsKey()); + } + } + } +} 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 0000000..b3317cf --- /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 0000000..f67ccf9 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/stats/Stats.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.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"; + 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"; +} 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 new file mode 100644 index 0000000..cc5de16 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/stats/StatsItem.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.common.stats; + +import java.util.LinkedList; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.LongAdder; + +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.logging.org.slf4j.Logger; + +public class StatsItem { + private final LongAdder value = new LongAdder(); + + private final LongAdder times = new LongAdder(); + + private final LinkedList csListMinute = new LinkedList<>(); + + private final LinkedList csListHour = new LinkedList<>(); + + private final LinkedList csListDay = new LinkedList<>(); + + private final String statsName; + private final String statsKey; + private long lastUpdateTimestamp = System.currentTimeMillis(); + private final ScheduledExecutorService scheduledExecutorService; + + private final Logger logger; + + public StatsItem(String statsName, String statsKey, ScheduledExecutorService scheduledExecutorService, Logger logger) { + this.statsName = statsName; + this.statsKey = statsKey; + this.scheduledExecutorService = scheduledExecutorService; + this.logger = logger; + } + + private static StatsSnapshot computeStatsData(final LinkedList csList) { + StatsSnapshot statsSnapshot = new StatsSnapshot(); + synchronized (csList) { + 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()); + + timesDiff = last.getTimes() - first.getTimes(); + if (timesDiff > 0) { + avgpt = (sum * 1.0d) / timesDiff; + } + } + + statsSnapshot.setSum(sum); + statsSnapshot.setTps(tps); + statsSnapshot.setAvgpt(avgpt); + statsSnapshot.setTimes(timesDiff); + } + + return statsSnapshot; + } + + public StatsSnapshot getStatsDataInMinute() { + return computeStatsData(this.csListMinute); + } + + public StatsSnapshot getStatsDataInHour() { + return computeStatsData(this.csListHour); + } + + public StatsSnapshot getStatsDataInDay() { + return computeStatsData(this.csListDay); + } + + public void init() { + + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + samplingInSeconds(); + } catch (Throwable ignored) { + } + } + }, 0, 10, TimeUnit.SECONDS); + + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + samplingInMinutes(); + } catch (Throwable ignored) { + } + } + }, 0, 10, TimeUnit.MINUTES); + + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + samplingInHour(); + } catch (Throwable ignored) { + } + } + }, 0, 1, TimeUnit.HOURS); + + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + printAtMinutes(); + } catch (Throwable ignored) { + } + } + }, Math.abs(UtilAll.computeNextMinutesTimeMillis() - System.currentTimeMillis()), 1000 * 60, TimeUnit.MILLISECONDS); + + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + printAtHour(); + } catch (Throwable ignored) { + } + } + }, Math.abs(UtilAll.computeNextHourTimeMillis() - System.currentTimeMillis()), 1000 * 60 * 60, TimeUnit.MILLISECONDS); + + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + printAtDay(); + } catch (Throwable ignored) { + } + } + }, Math.abs(UtilAll.computeNextMorningTimeMillis() - System.currentTimeMillis()) - 2000, 1000 * 60 * 60 * 24, TimeUnit.MILLISECONDS); + } + + public void samplingInSeconds() { + synchronized (this.csListMinute) { + 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(); + } + } + } + + public void samplingInMinutes() { + synchronized (this.csListHour) { + 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(); + } + } + } + + public void samplingInHour() { + synchronized (this.csListDay) { + 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(); + } + } + } + + public void printAtMinutes() { + StatsSnapshot ss = computeStatsData(this.csListMinute); + logger.info(String.format("[%s] [%s] Stats In One Minute, ", this.statsName, this.statsKey) + statPrintDetail(ss)); + } + + public void printAtHour() { + StatsSnapshot ss = computeStatsData(this.csListHour); + logger.info(String.format("[%s] [%s] Stats In One Hour, ", this.statsName, this.statsKey) + statPrintDetail(ss)); + + } + + public void printAtDay() { + StatsSnapshot ss = computeStatsData(this.csListDay); + 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 LongAdder getValue() { + return value; + } + + public String getStatsKey() { + return statsKey; + } + + public String getStatsName() { + return statsName; + } + + public LongAdder getTimes() { + return times; + } + + public long getLastUpdateTimestamp() { + return lastUpdateTimestamp; + } + + public void setLastUpdateTimestamp(long lastUpdateTimestamp) { + this.lastUpdateTimestamp = lastUpdateTimestamp; + } +} + +class CallSnapshot { + private final long timestamp; + private final long times; + + private final long value; + + public CallSnapshot(long timestamp, long times, long value) { + super(); + this.timestamp = timestamp; + this.times = times; + this.value = value; + } + + public long getTimestamp() { + return timestamp; + } + + public long getTimes() { + return times; + } + + public long getValue() { + return value; + } +} 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 new file mode 100644 index 0000000..8ed1486 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/stats/StatsItemSet.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.common.stats; + +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +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 StatsItemSet { + private static final Logger COMMERCIAL_LOG = LoggerFactory.getLogger(LoggerName.COMMERCIAL_LOGGER_NAME); + private final ConcurrentMap statsItemTable = + new ConcurrentHashMap<>(128); + + private final String statsName; + private final ScheduledExecutorService scheduledExecutorService; + + private final Logger logger; + + public StatsItemSet(String statsName, ScheduledExecutorService scheduledExecutorService, Logger logger) { + this.logger = logger; + this.statsName = statsName; + this.scheduledExecutorService = scheduledExecutorService; + this.init(); + } + + public void init() { + + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + samplingInSeconds(); + } catch (Throwable ignored) { + } + } + }, 0, 10, TimeUnit.SECONDS); + + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + samplingInMinutes(); + } catch (Throwable ignored) { + } + } + }, 0, 10, TimeUnit.MINUTES); + + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + samplingInHour(); + } catch (Throwable ignored) { + } + } + }, 0, 1, TimeUnit.HOURS); + + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + printAtMinutes(); + } catch (Throwable ignored) { + } + } + }, Math.abs(UtilAll.computeNextMinutesTimeMillis() - System.currentTimeMillis()), 1000 * 60, TimeUnit.MILLISECONDS); + + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + printAtHour(); + } catch (Throwable ignored) { + } + } + }, Math.abs(UtilAll.computeNextHourTimeMillis() - System.currentTimeMillis()), 1000 * 60 * 60, TimeUnit.MILLISECONDS); + + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + printAtDay(); + } catch (Throwable ignored) { + } + } + }, Math.abs(UtilAll.computeNextMorningTimeMillis() - System.currentTimeMillis()), 1000 * 60 * 60 * 24, TimeUnit.MILLISECONDS); + } + + private void samplingInSeconds() { + Iterator> it = this.statsItemTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + next.getValue().samplingInSeconds(); + } + } + + private void samplingInMinutes() { + Iterator> it = this.statsItemTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + next.getValue().samplingInMinutes(); + } + } + + private void samplingInHour() { + Iterator> it = this.statsItemTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + next.getValue().samplingInHour(); + } + } + + private void printAtMinutes() { + Iterator> it = this.statsItemTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + next.getValue().printAtMinutes(); + } + } + + private void printAtHour() { + Iterator> it = this.statsItemTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + next.getValue().printAtHour(); + } + } + + private void printAtDay() { + Iterator> it = this.statsItemTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + next.getValue().printAtDay(); + } + } + + public void addValue(final String statsKey, final int incValue, final int incTimes) { + StatsItem statsItem = this.getAndCreateStatsItem(statsKey); + statsItem.getValue().add(incValue); + statsItem.getTimes().add(incTimes); + statsItem.setLastUpdateTimestamp(System.currentTimeMillis()); + } + + 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); + statsItem.setLastUpdateTimestamp(System.currentTimeMillis()); + } + + 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) { + 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(); + } + } + + return statsItem; + } + + public StatsSnapshot getStatsDataInMinute(final String statsKey) { + StatsItem statsItem = this.statsItemTable.get(statsKey); + if (null != statsItem) { + return statsItem.getStatsDataInMinute(); + } + return new StatsSnapshot(); + } + + public StatsSnapshot getStatsDataInHour(final String statsKey) { + StatsItem statsItem = this.statsItemTable.get(statsKey); + if (null != statsItem) { + return statsItem.getStatsDataInHour(); + } + return new StatsSnapshot(); + } + + public StatsSnapshot getStatsDataInDay(final String statsKey) { + StatsItem statsItem = this.statsItemTable.get(statsKey); + if (null != statsItem) { + return statsItem.getStatsDataInDay(); + } + return new StatsSnapshot(); + } + + public StatsItem getStatsItem(final String statsKey) { + return this.statsItemTable.get(statsKey); + } + + + public void cleanResource(int maxStatsIdleTimeInMinutes) { + COMMERCIAL_LOG.info("CleanStatisticItemOld: kind:{}, size:{}", statsName, this.statsItemTable.size()); + Iterator> it = this.statsItemTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + StatsItem statsItem = next.getValue(); + if (System.currentTimeMillis() - statsItem.getLastUpdateTimestamp() > maxStatsIdleTimeInMinutes * 60 * 1000L) { + it.remove(); + COMMERCIAL_LOG.info("CleanStatisticItemOld: removeKind:{}, removeKey:{}", statsName, statsItem.getStatsKey()); + } + } + } +} 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 new file mode 100644 index 0000000..0cecce9 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/stats/StatsSnapshot.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.common.stats; + +public class StatsSnapshot { + private long sum; + private double tps; + + private long times; + private double avgpt; + + public long getSum() { + return sum; + } + + public void setSum(long sum) { + this.sum = sum; + } + + public double getTps() { + return tps; + } + + public void setTps(double tps) { + this.tps = tps; + } + + public double getAvgpt() { + return avgpt; + } + + 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/sysflag/MessageSysFlag.java b/common/src/main/java/org/apache/rocketmq/common/sysflag/MessageSysFlag.java new file mode 100644 index 0000000..bf60602 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/sysflag/MessageSysFlag.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.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; + } + + public static int resetTransactionValue(final int flag, final int type) { + return (flag & (~TRANSACTION_ROLLBACK_TYPE)) | 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 new file mode 100644 index 0000000..15d56dd --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/sysflag/PullSysFlag.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.common.sysflag; + +public class PullSysFlag { + 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) { + int flag = 0; + + if (commitOffset) { + flag |= FLAG_COMMIT_OFFSET; + } + + if (suspend) { + flag |= FLAG_SUSPEND; + } + + if (subscription) { + flag |= FLAG_SUBSCRIPTION; + } + + if (classFilter) { + flag |= FLAG_CLASS_FILTER; + } + + 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); + } + + public static boolean hasCommitOffsetFlag(final int sysFlag) { + return (sysFlag & FLAG_COMMIT_OFFSET) == FLAG_COMMIT_OFFSET; + } + + 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/SubscriptionSysFlag.java b/common/src/main/java/org/apache/rocketmq/common/sysflag/SubscriptionSysFlag.java new file mode 100644 index 0000000..81afb1b --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/sysflag/SubscriptionSysFlag.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.sysflag; + +public class SubscriptionSysFlag { + + private final static int FLAG_UNIT = 0x1 << 0; + + public static int buildSysFlag(final boolean unit) { + int sysFlag = 0; + + if (unit) { + sysFlag |= FLAG_UNIT; + } + + return sysFlag; + } + + public static int setUnitFlag(final int sysFlag) { + return sysFlag | FLAG_UNIT; + } + + public static int clearUnitFlag(final int sysFlag) { + return sysFlag & (~FLAG_UNIT); + } + + public static boolean hasUnitFlag(final int sysFlag) { + return (sysFlag & FLAG_UNIT) == FLAG_UNIT; + } + + public static void main(String[] args) { + } +} 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 new file mode 100644 index 0000000..a2bb508 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/sysflag/TopicSysFlag.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.sysflag; + +public class TopicSysFlag { + + private final static int FLAG_UNIT = 0x1 << 0; + + private final static int FLAG_UNIT_SUB = 0x1 << 1; + + public static int buildSysFlag(final boolean unit, final boolean hasUnitSub) { + int sysFlag = 0; + + if (unit) { + sysFlag |= FLAG_UNIT; + } + + if (hasUnitSub) { + sysFlag |= FLAG_UNIT_SUB; + } + + return sysFlag; + } + + public static int setUnitFlag(final int sysFlag) { + return sysFlag | FLAG_UNIT; + } + + public static int clearUnitFlag(final int sysFlag) { + return sysFlag & (~FLAG_UNIT); + } + + public static boolean hasUnitFlag(final int sysFlag) { + return (sysFlag & FLAG_UNIT) == FLAG_UNIT; + } + + public static int setUnitSubFlag(final int sysFlag) { + return sysFlag | FLAG_UNIT_SUB; + } + + public static int clearUnitSubFlag(final int sysFlag) { + return sysFlag & (~FLAG_UNIT_SUB); + } + + public static boolean hasUnitSubFlag(final int sysFlag) { + return (sysFlag & FLAG_UNIT_SUB) == FLAG_UNIT_SUB; + } +} 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 0000000..7b68873 --- /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 0000000..746128d --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/thread/ThreadPoolMonitor.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.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()); + String nameFormatted = String.format("%-40s", threadPoolWrapper.getName()); + String descFormatted = String.format("%-12s", monitor.describe()); + waterMarkLogger.info("{}{}{}", nameFormatted, descFormatted, 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 0000000..9e2e2f6 --- /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 0000000..548fec5 --- /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 0000000..e41859d --- /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 0000000..c19592a --- /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 0000000..78147b6 --- /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/AsyncShutdownHelper.java b/common/src/main/java/org/apache/rocketmq/common/utils/AsyncShutdownHelper.java new file mode 100644 index 0000000..da765d5 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/AsyncShutdownHelper.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.utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +public class AsyncShutdownHelper { + private final AtomicBoolean shutdown; + private final List targetList; + + private CountDownLatch countDownLatch; + + public AsyncShutdownHelper() { + this.targetList = new ArrayList<>(); + this.shutdown = new AtomicBoolean(false); + } + + public void addTarget(Shutdown target) { + if (shutdown.get()) { + return; + } + targetList.add(target); + } + + public AsyncShutdownHelper shutdown() { + if (shutdown.get()) { + return this; + } + if (targetList.isEmpty()) { + return this; + } + this.countDownLatch = new CountDownLatch(targetList.size()); + for (Shutdown target : targetList) { + Runnable runnable = () -> { + try { + target.shutdown(); + } catch (Exception ignored) { + + } finally { + countDownLatch.countDown(); + } + }; + new Thread(runnable).start(); + } + return this; + } + + public boolean await(long time, TimeUnit unit) throws InterruptedException { + if (shutdown.get()) { + return false; + } + try { + return this.countDownLatch.await(time, unit); + } finally { + shutdown.compareAndSet(false, true); + } + } +} 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 0000000..68d15e0 --- /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/ChannelUtil.java b/common/src/main/java/org/apache/rocketmq/common/utils/ChannelUtil.java new file mode 100644 index 0000000..adf49b2 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/ChannelUtil.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.common.utils; + +import io.netty.channel.Channel; +import java.net.InetAddress; +import java.net.InetSocketAddress; + +public class ChannelUtil { + public static String getRemoteIp(Channel channel) { + InetSocketAddress inetSocketAddress = (InetSocketAddress) channel.remoteAddress(); + if (inetSocketAddress == null) { + return ""; + } + final InetAddress inetAddr = inetSocketAddress.getAddress(); + return inetAddr != null ? inetAddr.getHostAddress() : inetSocketAddress.getHostName(); + } + +} 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 0000000..1cb85d0 --- /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 0000000..b73ece4 --- /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 0000000..3ab1cec --- /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 0000000..86d1fd4 --- /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 0000000..cc96770 --- /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/ExceptionUtils.java b/common/src/main/java/org/apache/rocketmq/common/utils/ExceptionUtils.java new file mode 100644 index 0000000..74acc8f --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/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.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/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 0000000..600054b --- /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/FutureUtils.java b/common/src/main/java/org/apache/rocketmq/common/utils/FutureUtils.java new file mode 100644 index 0000000..fb88b0a --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/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.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/common/src/main/java/org/apache/rocketmq/common/utils/HttpTinyClient.java b/common/src/main/java/org/apache/rocketmq/common/utils/HttpTinyClient.java new file mode 100755 index 0000000..85afc0a --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/HttpTinyClient.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.common.utils; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.util.Iterator; +import java.util.List; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.common.MixAll; + +public class HttpTinyClient { + + static public HttpResult httpGet(String url, List headers, List paramValues, + String encoding, long readTimeoutMs) throws IOException { + String encodedContent = encodingParams(paramValues, encoding); + url += (null == encodedContent) ? "" : ("?" + encodedContent); + + HttpURLConnection conn = null; + try { + conn = (HttpURLConnection) new URL(url).openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout((int) readTimeoutMs); + conn.setReadTimeout((int) readTimeoutMs); + setHeaders(conn, headers, encoding); + + conn.connect(); + int respCode = conn.getResponseCode(); + String resp = null; + + if (HttpURLConnection.HTTP_OK == respCode) { + resp = IOTinyUtils.toString(conn.getInputStream(), encoding); + } else { + resp = IOTinyUtils.toString(conn.getErrorStream(), encoding); + } + return new HttpResult(respCode, resp); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + } + + static private String encodingParams(List paramValues, String encoding) + throws UnsupportedEncodingException { + StringBuilder sb = new StringBuilder(); + if (null == paramValues) { + return null; + } + + for (Iterator iter = paramValues.iterator(); iter.hasNext(); ) { + sb.append(iter.next()).append("="); + sb.append(URLEncoder.encode(iter.next(), encoding)); + if (iter.hasNext()) { + sb.append("&"); + } + } + return sb.toString(); + } + + static private void setHeaders(HttpURLConnection conn, List headers, String encoding) { + if (null != headers) { + for (Iterator iter = headers.iterator(); iter.hasNext(); ) { + conn.addRequestProperty(iter.next(), iter.next()); + } + } + conn.addRequestProperty("Client-Version", MQVersion.getVersionDesc(MQVersion.CURRENT_VERSION)); + conn.addRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=" + encoding); + + String ts = String.valueOf(System.currentTimeMillis()); + conn.addRequestProperty("Metaq-Client-RequestTS", ts); + } + + /** + * @return the http response of given http post request + */ + static public HttpResult httpPost(String url, List headers, List paramValues, + String encoding, long readTimeoutMs) throws IOException { + String encodedContent = encodingParams(paramValues, encoding); + + HttpURLConnection conn = null; + try { + conn = (HttpURLConnection) new URL(url).openConnection(); + conn.setRequestMethod("POST"); + conn.setConnectTimeout(3000); + conn.setReadTimeout((int) readTimeoutMs); + conn.setDoOutput(true); + conn.setDoInput(true); + setHeaders(conn, headers, encoding); + + conn.getOutputStream().write(encodedContent.getBytes(MixAll.DEFAULT_CHARSET)); + + int respCode = conn.getResponseCode(); + String resp = null; + + if (HttpURLConnection.HTTP_OK == respCode) { + resp = IOTinyUtils.toString(conn.getInputStream(), encoding); + } else { + resp = IOTinyUtils.toString(conn.getErrorStream(), encoding); + } + return new HttpResult(respCode, resp); + } finally { + if (null != conn) { + conn.disconnect(); + } + } + } + + static public class HttpResult { + final public int code; + final public String content; + + public HttpResult(int code, String content) { + this.code = code; + this.content = content; + } + } +} 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 new file mode 100644 index 0000000..acba540 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/IOTinyUtils.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.utils; + +import java.io.BufferedReader; +import java.io.CharArrayWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +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; + +public class IOTinyUtils { + + static public String toString(InputStream input, String encoding) throws IOException { + return (null == encoding) ? toString(new InputStreamReader(input, StandardCharsets.UTF_8)) : toString(new InputStreamReader( + input, encoding)); + } + + static public String toString(Reader reader) throws IOException { + CharArrayWriter sw = new CharArrayWriter(); + copy(reader, sw); + return sw.toString(); + } + + static public long copy(Reader input, Writer output) throws IOException { + char[] buffer = new char[1 << 12]; + long count = 0; + for (int n = 0; (n = input.read(buffer)) >= 0; ) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + + static public List readLines(Reader input) throws IOException { + BufferedReader reader = toBufferedReader(input); + List list = new ArrayList<>(); + String line; + for (; ; ) { + line = reader.readLine(); + if (null != line) { + list.add(line); + } else { + break; + } + } + return list; + } + + static private BufferedReader toBufferedReader(Reader reader) { + return reader instanceof BufferedReader ? (BufferedReader) reader : new BufferedReader(reader); + } + + static public void copyFile(String source, String target) throws IOException { + File sf = new File(source); + if (!sf.exists()) { + throw new IllegalArgumentException("source file does not exist."); + } + File tf = new File(target); + tf.getParentFile().mkdirs(); + if (!tf.exists() && !tf.createNewFile()) { + throw new RuntimeException("failed to create target file."); + } + + FileChannel sc = null; + FileChannel tc = null; + try { + tc = new FileOutputStream(tf).getChannel(); + sc = new FileInputStream(sf).getChannel(); + sc.transferTo(0, sc.size(), tc); + } finally { + if (null != sc) { + sc.close(); + } + if (null != tc) { + tc.close(); + } + } + } + + public static void delete(File fileOrDir) throws IOException { + if (fileOrDir == null) { + return; + } + + if (fileOrDir.isDirectory()) { + cleanDirectory(fileOrDir); + } + + fileOrDir.delete(); + } + + public static void cleanDirectory(File directory) throws IOException { + if (!directory.exists()) { + String message = directory + " does not exist"; + throw new IllegalArgumentException(message); + } + + if (!directory.isDirectory()) { + String message = directory + " is not a directory"; + throw new IllegalArgumentException(message); + } + + File[] files = directory.listFiles(); + if (files == null) { // null if security restricted + throw new IOException("Failed to list contents of " + directory); + } + + IOException exception = null; + for (File file : files) { + try { + delete(file); + } catch (IOException ioe) { + exception = ioe; + } + } + + if (null != exception) { + throw exception; + } + } + + public static void writeStringToFile(File file, String data, String encoding) throws IOException { + OutputStream os = null; + try { + os = new FileOutputStream(file); + os.write(data.getBytes(encoding)); + } finally { + if (null != os) { + os.close(); + } + } + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/utils/IPAddressUtils.java b/common/src/main/java/org/apache/rocketmq/common/utils/IPAddressUtils.java new file mode 100644 index 0000000..5133219 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/IPAddressUtils.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.common.utils; + +import java.math.BigInteger; +import java.net.InetAddress; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.validator.routines.InetAddressValidator; + +public class IPAddressUtils { + + private static final String SLASH = "/"; + + private static final InetAddressValidator VALIDATOR = InetAddressValidator.getInstance(); + + public static boolean isValidIPOrCidr(String ipOrCidr) { + return isValidIp(ipOrCidr) || isValidCidr(ipOrCidr); + } + + public static boolean isValidIp(String ip) { + return VALIDATOR.isValid(ip); + } + + public static boolean isValidIPv4(String ip) { + return VALIDATOR.isValidInet4Address(ip); + } + + public static boolean isValidIPv6(String ip) { + return VALIDATOR.isValidInet6Address(ip); + } + + public static boolean isValidCidr(String cidr) { + return isValidIPv4Cidr(cidr) || isValidIPv6Cidr(cidr); + } + + public static boolean isValidIPv4Cidr(String cidr) { + try { + String[] parts = cidr.split(SLASH); + if (parts.length != 2) { + return false; + } + InetAddress ip = InetAddress.getByName(parts[0]); + if (ip.getAddress().length != 4) { + return false; + } + int prefix = Integer.parseInt(parts[1]); + return prefix >= 0 && prefix <= 32; + } catch (Exception e) { + return false; + } + } + + public static boolean isValidIPv6Cidr(String cidr) { + try { + String[] parts = cidr.split(SLASH); + if (parts.length != 2) { + return false; + } + InetAddress ip = InetAddress.getByName(parts[0]); + if (ip.getAddress().length != 16) { + return false; + } + int prefix = Integer.parseInt(parts[1]); + return prefix >= 0 && prefix <= 128; + } catch (Exception e) { + return false; + } + } + + public static boolean isIPInRange(String ip, String cidr) { + try { + String[] parts = cidr.split(SLASH); + if (parts.length == 1) { + return StringUtils.equals(ip, cidr); + } + if (parts.length != 2) { + return false; + } + InetAddress cidrIp = InetAddress.getByName(parts[0]); + int prefixLength = Integer.parseInt(parts[1]); + + BigInteger cidrIpBigInt = new BigInteger(1, cidrIp.getAddress()); + BigInteger ipBigInt = new BigInteger(1, InetAddress.getByName(ip).getAddress()); + + BigInteger mask = BigInteger.valueOf(-1).shiftLeft(cidrIp.getAddress().length * 8 - prefixLength); + BigInteger cidrIpLower = cidrIpBigInt.and(mask); + BigInteger cidrIpUpper = cidrIpLower.add(mask.not()); + + return ipBigInt.compareTo(cidrIpLower) >= 0 && ipBigInt.compareTo(cidrIpUpper) <= 0; + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} 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 0000000..a6563bc --- /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 0000000..68f8839 --- /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 0000000..2dc2a89 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/NetworkUtil.java @@ -0,0 +1,214 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.SocketAddress; +import java.net.SocketException; +import java.nio.channels.Selector; +import java.nio.channels.spi.SelectorProvider; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +import org.apache.commons.validator.routines.InetAddressValidator; +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 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"); + try { + final Method method = providerClazz.getMethod("provider"); + 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 List getLocalInetAddressList() throws SocketException { + Enumeration enumeration = NetworkInterface.getNetworkInterfaces(); + List inetAddressList = new ArrayList<>(); + // Traversal Network interface to get the non-bridge and non-virtual and non-ppp and up address + while (enumeration.hasMoreElements()) { + final NetworkInterface nif = enumeration.nextElement(); + if (isBridge(nif) || nif.isVirtual() || nif.isPointToPoint() || !nif.isUp()) { + continue; + } + InetAddressValidator validator = InetAddressValidator.getInstance(); + final Enumeration en = nif.getInetAddresses(); + while (en.hasMoreElements()) { + final InetAddress address = en.nextElement(); + if (address instanceof Inet4Address) { + byte[] ipByte = address.getAddress(); + if (ipByte.length == 4) { + if (validator.isValidInet4Address(UtilAll.ipToIPv4Str(ipByte))) { + inetAddressList.add(address); + } + } + } else if (address instanceof Inet6Address) { + byte[] ipByte = address.getAddress(); + if (ipByte.length == 16) { + if (validator.isValidInet6Address(UtilAll.ipToIPv6Str(ipByte))) { + inetAddressList.add(address); + } + } + } + } + } + return inetAddressList; + } + + public static InetAddress getLocalInetAddress() { + try { + ArrayList ipv4Result = new ArrayList<>(); + ArrayList ipv6Result = new ArrayList<>(); + List localInetAddressList = getLocalInetAddressList(); + for (InetAddress inetAddress : localInetAddressList) { + // Skip loopback addresses + if (inetAddress.isLoopbackAddress()) { + continue; + } + if (inetAddress instanceof Inet6Address) { + ipv6Result.add(inetAddress); + } else { + ipv4Result.add(inetAddress); + } + } + // prefer ipv4 and prefer external ip + if (!ipv4Result.isEmpty()) { + for (InetAddress ip : ipv4Result) { + if (UtilAll.isInternalIP(ip.getAddress())) { + continue; + } + return ip; + } + return ipv4Result.get(ipv4Result.size() - 1); + } else if (!ipv6Result.isEmpty()) { + for (InetAddress ip : ipv6Result) { + if (UtilAll.isInternalV6IP(ip)) { + continue; + } + return ip; + } + return ipv6Result.get(0); + } + //If failed to find,fall back to localhost + return InetAddress.getLocalHost(); + } catch (Exception e) { + log.error("Failed to obtain local address", e); + } + + return null; + } + + public static String getLocalAddress() { + InetAddress localHost = getLocalInetAddress(); + return normalizeHostAddress(localHost); + } + + 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); + return new InetSocketAddress(host, Integer.parseInt(port)); + } + + 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 0000000..105b88c --- /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 0000000..e2f006e --- /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 0000000..a98d245 --- /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 0000000..49e2c44 --- /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 overridden 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 0000000..07dc5f6 --- /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 0000000..1e700dd --- /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 0000000..2899938 --- /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 new file mode 100644 index 0000000..1644c63 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/ThreadUtils.java @@ -0,0 +1,223 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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 org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.constant.LoggerName; +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 LOGGER = LoggerFactory.getLogger(LoggerName.TOOLS_LOGGER_NAME); + + public static ExecutorService newSingleThreadExecutor(String processName, boolean isDaemon) { + return ThreadUtils.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 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 newScheduledThreadPool(int corePoolSize, String processName, + boolean 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("ThreadUtils-" + processName, isDaemon); + } + + public static ThreadFactory newGenericThreadFactory(String processName) { + return newGenericThreadFactory(processName, false); + } + + public static ThreadFactory newGenericThreadFactory(String processName, int threads) { + return newGenericThreadFactory(processName, threads, false); + } + + public static ThreadFactory newGenericThreadFactory(final String processName, final boolean isDaemon) { + return new ThreadFactoryImpl(processName + "_", isDaemon); + } + + public static ThreadFactory newGenericThreadFactory(final String processName, final int threads, + final boolean isDaemon) { + return new ThreadFactoryImpl(String.format("%s_%d_", processName, threads), isDaemon); + } + + /** + * Create a new 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? + * @return The unstarted thread + */ + public static Thread newThread(String name, Runnable runnable, boolean daemon) { + Thread thread = new Thread(runnable, name); + thread.setDaemon(daemon); + thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { + public void uncaughtException(Thread t, Throwable e) { + LOGGER.error("Uncaught exception in thread '" + t.getName() + "':", e); + } + }); + return thread; + } + + /** + * Shutdown passed thread using isAlive and join. + * + * @param t Thread to stop + */ + public static void shutdownGracefully(final Thread t) { + shutdownGracefully(t, 0); + } + + /** + * Shutdown passed thread using isAlive and join. + * + * @param millis Pass 0 if we're to wait forever. + * @param t Thread to stop + */ + public static void shutdownGracefully(final Thread t, final long millis) { + if (t == null) + return; + while (t.isAlive()) { + try { + t.interrupt(); + t.join(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + /** + * An implementation of the graceful stop sequence recommended by + * {@link ExecutorService}. + * + * @param executor executor + * @param timeout timeout + * @param timeUnit timeUnit + */ + public static void shutdownGracefully(ExecutorService executor, long timeout, TimeUnit timeUnit) { + // Disable new tasks from being submitted. + executor.shutdown(); + try { + // Wait a while for existing tasks to terminate. + if (!executor.awaitTermination(timeout, timeUnit)) { + executor.shutdownNow(); + // Wait a while for tasks to respond to being cancelled. + if (!executor.awaitTermination(timeout, timeUnit)) { + LOGGER.warn(String.format("%s didn't terminate!", executor)); + } + } + } catch (InterruptedException ie) { + // (Re-)Cancel if current thread also interrupted. + executor.shutdownNow(); + // Preserve interrupt status. + Thread.currentThread().interrupt(); + } + } + + /** + * 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. + */ + private ThreadUtils() { + // Unused + + } +} 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 0000000..b90cd30 --- /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/BrokerConfigSingletonTest.java b/common/src/test/java/org/apache/rocketmq/common/BrokerConfigSingletonTest.java new file mode 100644 index 0000000..b98a6e3 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/BrokerConfigSingletonTest.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; + +import org.junit.Assert; +import org.junit.Test; + +public class BrokerConfigSingletonTest { + + /** + * Tests the behavior of getting the broker configuration when it has not been initialized. + * Expects an IllegalArgumentException to be thrown, ensuring that the configuration cannot be obtained without initialization. + */ + @Test(expected = IllegalArgumentException.class) + public void getBrokerConfig_NullConfiguration_ThrowsException() { + BrokerConfigSingleton.getBrokerConfig(); + } + + /** + * Tests the behavior of setting the broker configuration after it has already been initialized. + * Expects an IllegalArgumentException to be thrown, ensuring that the configuration cannot be reset once set. + * Also asserts that the returned brokerConfig instance is the same as the one set, confirming the singleton property. + */ + @Test(expected = IllegalArgumentException.class) + public void setBrokerConfig_AlreadyInitialized_ThrowsException() { + BrokerConfig config = new BrokerConfig(); + BrokerConfigSingleton.setBrokerConfig(config); + Assert.assertSame("Expected brokerConfig instance is not returned", config, BrokerConfigSingleton.getBrokerConfig()); + BrokerConfigSingleton.setBrokerConfig(config); + } + +} diff --git a/common/src/test/java/org/apache/rocketmq/common/BrokerConfigTest.java b/common/src/test/java/org/apache/rocketmq/common/BrokerConfigTest.java new file mode 100644 index 0000000..07e132f --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/BrokerConfigTest.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.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BrokerConfigTest { + + @Test + 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 0000000..a61ec4c --- /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 0000000..3570db6 --- /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 0000000..47191c9 --- /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/MQVersionTest.java b/common/src/test/java/org/apache/rocketmq/common/MQVersionTest.java new file mode 100644 index 0000000..51856b8 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/MQVersionTest.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; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MQVersionTest { + + @Test + public void testGetVersionDesc() throws Exception { + String desc = "V3_0_0_SNAPSHOT"; + assertThat(MQVersion.getVersionDesc(0)).isEqualTo(desc); + } + + @Test + public void testGetVersionDesc_higherVersion() throws Exception { + String desc = "HIGHER_VERSION"; + assertThat(MQVersion.getVersionDesc(Integer.MAX_VALUE)).isEqualTo(desc); + } + + @Test + public void testValue2Version() throws Exception { + assertThat(MQVersion.value2Version(0)).isEqualTo(MQVersion.Version.V3_0_0_SNAPSHOT); + } + + @Test + public void testValue2Version_HigherVersion() throws Exception { + assertThat(MQVersion.value2Version(Integer.MAX_VALUE)).isEqualTo(MQVersion.Version.HIGHER_VERSION); + } +} \ 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 new file mode 100644 index 0000000..5876cbd --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/MessageBatchTest.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.common; + +import java.util.ArrayList; +import java.util.List; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageBatch; +import org.junit.Test; + +public class MessageBatchTest { + + public List generateMessages() { + List messages = new ArrayList<>(); + Message message1 = new Message("topic1", "body".getBytes()); + Message message2 = new Message("topic1", "body".getBytes()); + + messages.add(message1); + messages.add(message2); + return messages; + } + + @Test + public void testGenerate_OK() throws Exception { + List messages = generateMessages(); + MessageBatch.generateFromList(messages); + } + + @Test(expected = UnsupportedOperationException.class) + public void testGenerate_DiffTopic() throws Exception { + List messages = generateMessages(); + messages.get(1).setTopic("topic2"); + MessageBatch.generateFromList(messages); + } + + @Test(expected = UnsupportedOperationException.class) + public void testGenerate_DiffWaitOK() throws Exception { + List messages = generateMessages(); + messages.get(1).setWaitStoreMsgOK(false); + MessageBatch.generateFromList(messages); + } + + @Test(expected = UnsupportedOperationException.class) + public void testGenerate_Delay() throws Exception { + List messages = generateMessages(); + messages.get(1).setDelayTimeLevel(1); + MessageBatch.generateFromList(messages); + } + + @Test(expected = UnsupportedOperationException.class) + public void testGenerate_Retry() throws Exception { + List messages = generateMessages(); + messages.get(1).setTopic(MixAll.RETRY_GROUP_TOPIC_PREFIX + "topic"); + MessageBatch.generateFromList(messages); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/MessageEncodeDecodeTest.java b/common/src/test/java/org/apache/rocketmq/common/MessageEncodeDecodeTest.java new file mode 100644 index 0000000..7c73147 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/MessageEncodeDecodeTest.java @@ -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. + */ +package org.apache.rocketmq.common; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +public class MessageEncodeDecodeTest { + + @Test + public void testEncodeDecodeSingle() throws Exception { + Message message = new Message("topic", "body".getBytes()); + message.setFlag(12); + message.putUserProperty("key", "value"); + byte[] bytes = MessageDecoder.encodeMessage(message); + ByteBuffer buffer = ByteBuffer.allocate(bytes.length); + buffer.put(bytes); + buffer.flip(); + Message newMessage = MessageDecoder.decodeMessage(buffer); + + assertTrue(message.getFlag() == newMessage.getFlag()); + assertTrue(newMessage.getProperty("key").equals(newMessage.getProperty("key"))); + assertTrue(Arrays.equals(newMessage.getBody(), message.getBody())); + } + + @Test + public void testEncodeDecodeList() throws Exception { + List messages = new ArrayList<>(128); + for (int i = 0; i < 100; i++) { + Message message = new Message("topic", ("body" + i).getBytes()); + message.setFlag(i); + message.putUserProperty("key", "value" + i); + messages.add(message); + } + byte[] bytes = MessageDecoder.encodeMessages(messages); + + ByteBuffer buffer = ByteBuffer.allocate(bytes.length); + buffer.put(bytes); + buffer.flip(); + + List newMsgs = MessageDecoder.decodeMessages(buffer); + + assertTrue(newMsgs.size() == messages.size()); + + for (int i = 0; i < newMsgs.size(); i++) { + Message message = messages.get(i); + Message newMessage = newMsgs.get(i); + assertTrue(message.getFlag() == newMessage.getFlag()); + assertTrue(newMessage.getProperty("key").equals(newMessage.getProperty("key"))); + assertTrue(Arrays.equals(newMessage.getBody(), message.getBody())); + + } + } +} 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 0000000..77d69e5 --- /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 new file mode 100644 index 0000000..5b358ca --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/MixAllTest.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; + +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; + +public class MixAllTest { + @Test + public void testGetLocalInetAddress() throws Exception { + List localInetAddress = MixAll.getLocalInetAddress(); + String local = InetAddress.getLocalHost().getHostAddress(); + assertThat(localInetAddress).contains("127.0.0.1"); + assertThat(local).isNotNull(); + } + + @Test + public void testBrokerVIPChannel() { + assertThat(MixAll.brokerVIPChannel(true, "127.0.0.1:10911")).isEqualTo("127.0.0.1:10909"); + } + + @Test + public void testCompareAndIncreaseOnly() { + AtomicLong target = new AtomicLong(5); + assertThat(MixAll.compareAndIncreaseOnly(target, 6)).isTrue(); + assertThat(target.get()).isEqualTo(6); + + assertThat(MixAll.compareAndIncreaseOnly(target, 4)).isFalse(); + assertThat(target.get()).isEqualTo(6); + } + + @Test + public void testFile2String() 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(); + MixAll.string2File("MixAll_testString2File", fileName); + assertThat(MixAll.file2String(fileName)).isEqualTo("MixAll_testString2File"); + } + + @Test + 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 0000000..a1b2253 --- /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/ServiceThreadTest.java b/common/src/test/java/org/apache/rocketmq/common/ServiceThreadTest.java new file mode 100644 index 0000000..93208bc --- /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 0000000..3df93a0 --- /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 new file mode 100644 index 0000000..a2b498f --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/UtilAllTest.java @@ -0,0 +1,283 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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() { + String currentStackTrace = UtilAll.currentStackTrace(); + assertThat(currentStackTrace).contains("UtilAll.currentStackTrace"); + assertThat(currentStackTrace).contains("UtilAllTest.testCurrentStackTrace("); + } + + @Test + public void testProperties2Object() { + DemoConfig demoConfig = new DemoConfig(); + Properties properties = new Properties(); + properties.setProperty("demoWidth", "123"); + properties.setProperty("demoLength", "456"); + properties.setProperty("demoOK", "true"); + properties.setProperty("demoName", "TestDemo"); + MixAll.properties2Object(properties, demoConfig); + assertThat(demoConfig.getDemoLength()).isEqualTo(456); + assertThat(demoConfig.getDemoWidth()).isEqualTo(123); + assertThat(demoConfig.isDemoOK()).isTrue(); + assertThat(demoConfig.getDemoName()).isEqualTo("TestDemo"); + } + + @Test + public void testProperties2String() { + 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 + public void testIsPropertiesEqual() { + final Properties p1 = new Properties(); + final Properties p2 = new Properties(); + + p1.setProperty("a", "1"); + p1.setProperty("b", "2"); + p2.setProperty("a", "1"); + p2.setProperty("b", "2"); + + assertThat(MixAll.isPropertiesEqual(p1, p2)).isTrue(); + } + + @Test + public void testGetPid() { + assertThat(UtilAll.getPid()).isGreaterThan(0); + } + + @Test + public void testGetDiskPartitionSpaceUsedPercent() { + String tmpDir = System.getProperty("java.io.tmpdir"); + + assertThat(UtilAll.getDiskPartitionSpaceUsedPercent(null)).isCloseTo(-1, within(0.000001)); + assertThat(UtilAll.getDiskPartitionSpaceUsedPercent("")).isCloseTo(-1, within(0.000001)); + assertThat(UtilAll.getDiskPartitionSpaceUsedPercent("nonExistingPath")).isCloseTo(-1, within(0.000001)); + assertThat(UtilAll.getDiskPartitionSpaceUsedPercent(tmpDir)).isNotCloseTo(-1, within(0.000001)); + } + + @Test + public void testIsBlank() { + assertThat(UtilAll.isBlank("Hello ")).isFalse(); + assertThat(UtilAll.isBlank(" Hello")).isFalse(); + assertThat(UtilAll.isBlank("He llo")).isFalse(); + assertThat(UtilAll.isBlank(" ")).isTrue(); + 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; + private boolean demoOK = false; + private String demoName = "haha"; + + int getDemoWidth() { + return demoWidth; + } + + public void setDemoWidth(int demoWidth) { + this.demoWidth = demoWidth; + } + + public int getDemoLength() { + return demoLength; + } + + public void setDemoLength(int demoLength) { + this.demoLength = demoLength; + } + + public boolean isDemoOK() { + return demoOK; + } + + public void setDemoOK(boolean demoOK) { + this.demoOK = demoOK; + } + + public String getDemoName() { + return demoName; + } + + public void setDemoName(String demoName) { + this.demoName = demoName; + } + + @Override + public String toString() { + return "DemoConfig{" + + "demoWidth=" + demoWidth + + ", demoLength=" + demoLength + + ", demoOK=" + demoOK + + ", demoName='" + demoName + '\'' + + '}'; + } + } + + 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/action/ActionTest.java b/common/src/test/java/org/apache/rocketmq/common/action/ActionTest.java new file mode 100644 index 0000000..e3c1b9a --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/action/ActionTest.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.action; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class ActionTest { + + @Test + public void getByName_NullName_ReturnsNull() { + Action result = Action.getByName("null"); + assertNull(result); + } + + @Test + public void getByName_UnknownName_ReturnsUnknown() { + Action result = Action.getByName("unknown"); + assertEquals(Action.UNKNOWN, result); + } + + @Test + public void getByName_AllName_ReturnsAll() { + Action result = Action.getByName("All"); + assertEquals(Action.ALL, result); + } + + @Test + public void getByName_AnyName_ReturnsAny() { + Action result = Action.getByName("Any"); + assertEquals(Action.ANY, result); + } + + @Test + public void getByName_PubName_ReturnsPub() { + Action result = Action.getByName("Pub"); + assertEquals(Action.PUB, result); + } + + @Test + public void getByName_SubName_ReturnsSub() { + Action result = Action.getByName("Sub"); + assertEquals(Action.SUB, result); + } + + @Test + public void getByName_CreateName_ReturnsCreate() { + Action result = Action.getByName("Create"); + assertEquals(Action.CREATE, result); + } + + @Test + public void getByName_UpdateName_ReturnsUpdate() { + Action result = Action.getByName("Update"); + assertEquals(Action.UPDATE, result); + } + + @Test + public void getByName_DeleteName_ReturnsDelete() { + Action result = Action.getByName("Delete"); + assertEquals(Action.DELETE, result); + } + + @Test + public void getByName_GetName_ReturnsGet() { + Action result = Action.getByName("Get"); + assertEquals(Action.GET, result); + } + + @Test + public void getByName_ListName_ReturnsList() { + Action result = Action.getByName("List"); + assertEquals(Action.LIST, result); + } + + @Test + public void getCode_ReturnsCorrectCode() { + assertEquals((byte) 1, Action.ALL.getCode()); + assertEquals((byte) 2, Action.ANY.getCode()); + assertEquals((byte) 3, Action.PUB.getCode()); + assertEquals((byte) 4, Action.SUB.getCode()); + assertEquals((byte) 5, Action.CREATE.getCode()); + assertEquals((byte) 6, Action.UPDATE.getCode()); + assertEquals((byte) 7, Action.DELETE.getCode()); + assertEquals((byte) 8, Action.GET.getCode()); + assertEquals((byte) 9, Action.LIST.getCode()); + } + + @Test + public void getName_ReturnsCorrectName() { + assertEquals("All", Action.ALL.getName()); + assertEquals("Any", Action.ANY.getName()); + assertEquals("Pub", Action.PUB.getName()); + assertEquals("Sub", Action.SUB.getName()); + assertEquals("Create", Action.CREATE.getName()); + assertEquals("Update", Action.UPDATE.getName()); + assertEquals("Delete", Action.DELETE.getName()); + assertEquals("Get", Action.GET.getName()); + assertEquals("List", Action.LIST.getName()); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/action/RocketMQActionTest.java b/common/src/test/java/org/apache/rocketmq/common/action/RocketMQActionTest.java new file mode 100644 index 0000000..373a1f6 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/action/RocketMQActionTest.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.action; + +import org.apache.rocketmq.common.resource.ResourceType; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public class RocketMQActionTest { + + @Test + public void testRocketMQAction_DefaultResourceType_CustomisedValueAndActionArray() { + RocketMQAction annotation = DemoClass.class.getAnnotation(RocketMQAction.class); + assertEquals(0, annotation.value()); + assertEquals(ResourceType.UNKNOWN, annotation.resource()); + assertArrayEquals(new Action[] {}, annotation.action()); + } + + @Test + public void testRocketMQAction_CustomisedValueAndResourceTypeAndActionArray() { + RocketMQAction annotation = CustomisedDemoClass.class.getAnnotation(RocketMQAction.class); + assertEquals(1, annotation.value()); + assertEquals(ResourceType.TOPIC, annotation.resource()); + assertArrayEquals(new Action[] {Action.CREATE, Action.DELETE}, annotation.action()); + } + + @RocketMQAction(value = 0, resource = ResourceType.UNKNOWN, action = {}) + private static class DemoClass { + } + + @RocketMQAction(value = 1, resource = ResourceType.TOPIC, action = {Action.CREATE, Action.DELETE}) + private static class CustomisedDemoClass { + } +} 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 0000000..a895873 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/attribute/AttributeParserTest.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.common.attribute; + +import com.google.common.collect.Maps; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Assert; +import org.junit.Test; + +import static com.google.common.collect.Maps.newHashMap; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class AttributeParserTest { + + @Test + public void parseToMap_EmptyString_ReturnsEmptyMap() { + String attributesModification = ""; + Map result = AttributeParser.parseToMap(attributesModification); + assertTrue(result.isEmpty()); + } + + @Test + public void parseToMap_NullString_ReturnsEmptyMap() { + String attributesModification = null; + Map result = AttributeParser.parseToMap(attributesModification); + assertTrue(result.isEmpty()); + } + + @Test + public void parseToMap_ValidAttributesModification_ReturnsExpectedMap() { + String attributesModification = "+key1=value1,+key2=value2,-key3,+key4=value4"; + Map result = AttributeParser.parseToMap(attributesModification); + + Map expectedMap = new HashMap<>(); + expectedMap.put("+key1", "value1"); + expectedMap.put("+key2", "value2"); + expectedMap.put("-key3", ""); + expectedMap.put("+key4", "value4"); + + assertEquals(expectedMap, result); + } + + @Test(expected = RuntimeException.class) + public void parseToMap_InvalidAddAttributeFormat_ThrowsRuntimeException() { + String attributesModification = "+key1=value1,key2=value2,-key3,+key4=value4"; + AttributeParser.parseToMap(attributesModification); + } + + @Test(expected = RuntimeException.class) + public void parseToMap_InvalidDeleteAttributeFormat_ThrowsRuntimeException() { + String attributesModification = "+key1=value1,+key2=value2,key3,+key4=value4"; + AttributeParser.parseToMap(attributesModification); + } + + @Test(expected = RuntimeException.class) + public void parseToMap_DuplicateKey_ThrowsRuntimeException() { + String attributesModification = "+key1=value1,+key1=value2"; + AttributeParser.parseToMap(attributesModification); + } + + @Test + public void parseToString_EmptyMap_ReturnsEmptyString() { + Map attributes = new HashMap<>(); + String result = AttributeParser.parseToString(attributes); + assertEquals("", result); + } + + @Test + public void parseToString_ValidAttributes_ReturnsExpectedString() { + Map attributes = new HashMap<>(); + attributes.put("key1", "value1"); + attributes.put("key2", "value2"); + attributes.put("key3", ""); + + String result = AttributeParser.parseToString(attributes); + String expectedString = "key1=value1,key2=value2,key3"; + assertEquals(expectedString, result); + } + + @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 0000000..9be0f31 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/attribute/AttributeTest.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.attribute; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.collect.Sets.newHashSet; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class AttributeTest { + + private Attribute attribute; + + @Before + public void setUp() { + attribute = new Attribute("testAttribute", true) { + @Override + public void verify(String value) { + throw new UnsupportedOperationException(); + } + }; + } + + @Test + public void testGetName_ShouldReturnCorrectName() { + assertEquals("testAttribute", attribute.getName()); + } + + @Test + public void testSetName_ShouldSetCorrectName() { + attribute.setName("newTestAttribute"); + assertEquals("newTestAttribute", attribute.getName()); + } + + @Test + public void testIsChangeable_ShouldReturnCorrectChangeableStatus() { + assertTrue(attribute.isChangeable()); + } + + @Test + public void testSetChangeable_ShouldSetCorrectChangeableStatus() { + attribute.setChangeable(false); + assertFalse(attribute.isChangeable()); + } + + @Test(expected = UnsupportedOperationException.class) + public void testVerify_ShouldThrowUnsupportedOperationException() { + attribute.verify("testValue"); + } + + @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/attribute/AttributeUtilTest.java b/common/src/test/java/org/apache/rocketmq/common/attribute/AttributeUtilTest.java new file mode 100644 index 0000000..aef46c1 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/attribute/AttributeUtilTest.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.common.attribute; + +import com.google.common.collect.ImmutableMap; +import java.util.HashMap; +import java.util.Map; +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 AttributeUtilTest { + + private Map allAttributes; + private ImmutableMap currentAttributes; + + @Before + public void setUp() { + allAttributes = new HashMap<>(); + allAttributes.put("attr1", new TestAttribute("value1", true, value -> true)); + allAttributes.put("attr2", new TestAttribute("value2", true, value -> true)); + allAttributes.put("attr3", new TestAttribute("value3", true, value -> value.equals("valid"))); + + currentAttributes = ImmutableMap.of("attr1", "value1", "attr2", "value2"); + } + + @Test + public void alterCurrentAttributes_CreateMode_ShouldReturnOnlyAddedAttributes() { + ImmutableMap newAttributes = ImmutableMap.of("+attr1", "new_value1", "+attr2", "value2", "+attr3", "value3"); + + Map result = AttributeUtil.alterCurrentAttributes(true, allAttributes, currentAttributes, newAttributes); + + assertEquals(3, result.size()); + assertTrue(result.containsKey("attr1")); + assertEquals("new_value1", result.get("attr1")); + assertTrue(result.containsKey("attr3")); + assertEquals("value3", result.get("attr3")); + assertTrue(result.containsKey("attr2")); + } + + @Test(expected = RuntimeException.class) + public void alterCurrentAttributes_CreateMode_AddNonAddableAttribute_ShouldThrowException() { + ImmutableMap newAttributes = ImmutableMap.of("attr1", "value1"); + AttributeUtil.alterCurrentAttributes(true, allAttributes, currentAttributes, newAttributes); + } + + @Test + public void alterCurrentAttributes_UpdateMode_ShouldReturnUpdatedAndAddedAttributes() { + ImmutableMap newAttributes = ImmutableMap.of("+attr1", "new_value1", "-attr2", "value2", "+attr3", "value3"); + + Map result = AttributeUtil.alterCurrentAttributes(false, allAttributes, currentAttributes, newAttributes); + + assertEquals(2, result.size()); + assertTrue(result.containsKey("attr1")); + assertEquals("new_value1", result.get("attr1")); + assertTrue(result.containsKey("attr3")); + assertEquals("value3", result.get("attr3")); + assertFalse(result.containsKey("attr2")); + } + + @Test(expected = RuntimeException.class) + public void alterCurrentAttributes_UpdateMode_DeleteNonExistentAttribute_ShouldThrowException() { + ImmutableMap newAttributes = ImmutableMap.of("-attr4", "value4"); + AttributeUtil.alterCurrentAttributes(false, allAttributes, currentAttributes, newAttributes); + } + + @Test(expected = RuntimeException.class) + public void alterCurrentAttributes_UpdateMode_WrongFormatKey_ShouldThrowException() { + ImmutableMap newAttributes = ImmutableMap.of("attr1", "+value1"); + AttributeUtil.alterCurrentAttributes(false, allAttributes, currentAttributes, newAttributes); + } + + @Test(expected = RuntimeException.class) + public void alterCurrentAttributes_UnsupportedKey_ShouldThrowException() { + ImmutableMap newAttributes = ImmutableMap.of("unsupported_attr", "value"); + AttributeUtil.alterCurrentAttributes(false, allAttributes, currentAttributes, newAttributes); + } + + @Test(expected = RuntimeException.class) + public void alterCurrentAttributes_AttemptToUpdateUnchangeableAttribute_ShouldThrowException() { + ImmutableMap newAttributes = ImmutableMap.of("attr2", "new_value2"); + AttributeUtil.alterCurrentAttributes(false, allAttributes, currentAttributes, newAttributes); + } + + private static class TestAttribute extends Attribute { + private final AttributeValidator validator; + + public TestAttribute(String name, boolean changeable, AttributeValidator validator) { + super(name, changeable); + this.validator = validator; + } + + @Override + public void verify(String value) { + validator.validate(value); + } + } + + private interface AttributeValidator { + boolean validate(String value); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/attribute/BooleanAttributeTest.java b/common/src/test/java/org/apache/rocketmq/common/attribute/BooleanAttributeTest.java new file mode 100644 index 0000000..6bed6ff --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/attribute/BooleanAttributeTest.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.common.attribute; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; + +public class BooleanAttributeTest { + + private BooleanAttribute booleanAttribute; + + @Before + public void setUp() { + booleanAttribute = new BooleanAttribute("testAttribute", true, false); + } + + @Test + public void testVerify_ValidValue_NoExceptionThrown() { + booleanAttribute.verify("true"); + booleanAttribute.verify("false"); + } + + @Test + public void testVerify_InvalidValue_ExceptionThrown() { + assertThrows(RuntimeException.class, () -> booleanAttribute.verify("invalid")); + assertThrows(RuntimeException.class, () -> booleanAttribute.verify("1")); + assertThrows(RuntimeException.class, () -> booleanAttribute.verify("0")); + assertThrows(RuntimeException.class, () -> booleanAttribute.verify("")); + } + + @Test + public void testGetDefaultValue() { + assertFalse(booleanAttribute.getDefaultValue()); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/attribute/CQTypeTest.java b/common/src/test/java/org/apache/rocketmq/common/attribute/CQTypeTest.java new file mode 100644 index 0000000..41aa98b --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/attribute/CQTypeTest.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; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class CQTypeTest { + + @Test + public void testValues() { + CQType[] values = CQType.values(); + assertEquals(3, values.length); + assertEquals(CQType.SimpleCQ, values[0]); + assertEquals(CQType.BatchCQ, values[1]); + assertEquals(CQType.RocksDBCQ, values[2]); + } + + @Test + public void testValueOf() { + assertEquals(CQType.SimpleCQ, CQType.valueOf("SimpleCQ")); + assertEquals(CQType.BatchCQ, CQType.valueOf("BatchCQ")); + assertEquals(CQType.RocksDBCQ, CQType.valueOf("RocksDBCQ")); + } + + @Test(expected = IllegalArgumentException.class) + public void testValueOf_InvalidName() { + CQType.valueOf("InvalidCQ"); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/attribute/CleanupPolicyTest.java b/common/src/test/java/org/apache/rocketmq/common/attribute/CleanupPolicyTest.java new file mode 100644 index 0000000..584de2b --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/attribute/CleanupPolicyTest.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.attribute; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class CleanupPolicyTest { + + @Test + public void testCleanupPolicy_Delete() { + CleanupPolicy cleanupPolicy = CleanupPolicy.DELETE; + assertEquals("DELETE", cleanupPolicy.toString()); + } + + @Test + public void testCleanupPolicy_Compaction() { + CleanupPolicy cleanupPolicy = CleanupPolicy.COMPACTION; + assertEquals("COMPACTION", cleanupPolicy.toString()); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/attribute/EnumAttributeTest.java b/common/src/test/java/org/apache/rocketmq/common/attribute/EnumAttributeTest.java new file mode 100644 index 0000000..637dc30 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/attribute/EnumAttributeTest.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.attribute; + +import org.junit.Before; +import org.junit.Test; +import java.util.HashSet; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class EnumAttributeTest { + + private EnumAttribute enumAttribute; + + @Before + public void setUp() { + Set universe = new HashSet<>(); + universe.add("value1"); + universe.add("value2"); + universe.add("value3"); + + enumAttribute = new EnumAttribute("testAttribute", true, universe, "value1"); + } + + @Test + public void verify_ValidValue_NoExceptionThrown() { + enumAttribute.verify("value1"); + enumAttribute.verify("value2"); + enumAttribute.verify("value3"); + } + + @Test + public void verify_InvalidValue_ExceptionThrown() { + RuntimeException exception = assertThrows(RuntimeException.class, () -> { + enumAttribute.verify("invalidValue"); + }); + + assertTrue(exception.getMessage().startsWith("value is not in set:")); + } + + @Test + public void getDefaultValue_ReturnsDefaultValue() { + assertEquals("value1", enumAttribute.getDefaultValue()); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/attribute/LongRangeAttributeTest.java b/common/src/test/java/org/apache/rocketmq/common/attribute/LongRangeAttributeTest.java new file mode 100644 index 0000000..222f909 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/attribute/LongRangeAttributeTest.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.common.attribute; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +public class LongRangeAttributeTest { + + private LongRangeAttribute longRangeAttribute; + + @Before + public void setUp() { + longRangeAttribute = new LongRangeAttribute("testAttribute", true, 0, 100, 50); + } + + @Test + public void verify_ValidValue_NoExceptionThrown() { + longRangeAttribute.verify("50"); + } + + @Test + public void verify_MinValue_NoExceptionThrown() { + longRangeAttribute.verify("0"); + } + + @Test + public void verify_MaxValue_NoExceptionThrown() { + longRangeAttribute.verify("100"); + } + + @Test + public void verify_ValueLessThanMin_ThrowsRuntimeException() { + RuntimeException exception = assertThrows(RuntimeException.class, () -> longRangeAttribute.verify("-1")); + assertEquals("value is not in range(0, 100)", exception.getMessage()); + } + + @Test + public void verify_ValueGreaterThanMax_ThrowsRuntimeException() { + RuntimeException exception = assertThrows(RuntimeException.class, () -> longRangeAttribute.verify("101")); + assertEquals("value is not in range(0, 100)", exception.getMessage()); + } + + @Test + public void getDefaultValue_ReturnsDefaultValue() { + assertEquals(50, longRangeAttribute.getDefaultValue()); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/attribute/TopicMessageTypeTest.java b/common/src/test/java/org/apache/rocketmq/common/attribute/TopicMessageTypeTest.java new file mode 100644 index 0000000..0321679 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/attribute/TopicMessageTypeTest.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.attribute; + +import com.google.common.collect.Sets; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import org.apache.rocketmq.common.message.MessageConst; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class TopicMessageTypeTest { + + private Map normalMessageProperty; + private Map transactionMessageProperty; + private Map delayMessageProperty; + private Map fifoMessageProperty; + + @Before + public void setUp() { + normalMessageProperty = new HashMap<>(); + transactionMessageProperty = new HashMap<>(); + delayMessageProperty = new HashMap<>(); + fifoMessageProperty = new HashMap<>(); + + transactionMessageProperty.put(MessageConst.PROPERTY_TRANSACTION_PREPARED, "true"); + delayMessageProperty.put(MessageConst.PROPERTY_DELAY_TIME_LEVEL, "1"); + fifoMessageProperty.put(MessageConst.PROPERTY_SHARDING_KEY, "shardingKey"); + } + + @Test + public void testTopicMessageTypeSet() { + Set expectedSet = Sets.newHashSet("UNSPECIFIED", "NORMAL", "FIFO", "DELAY", "TRANSACTION", "MIXED"); + Set actualSet = TopicMessageType.topicMessageTypeSet(); + assertEquals(expectedSet, actualSet); + } + + @Test + public void testParseFromMessageProperty_Normal() { + TopicMessageType actual = TopicMessageType.parseFromMessageProperty(normalMessageProperty); + assertEquals(TopicMessageType.NORMAL, actual); + } + + @Test + public void testParseFromMessageProperty_Transaction() { + TopicMessageType actual = TopicMessageType.parseFromMessageProperty(transactionMessageProperty); + assertEquals(TopicMessageType.TRANSACTION, actual); + } + + @Test + public void testParseFromMessageProperty_Delay() { + TopicMessageType actual = TopicMessageType.parseFromMessageProperty(delayMessageProperty); + assertEquals(TopicMessageType.DELAY, actual); + } + + @Test + public void testParseFromMessageProperty_Fifo() { + TopicMessageType actual = TopicMessageType.parseFromMessageProperty(fifoMessageProperty); + assertEquals(TopicMessageType.FIFO, actual); + } + + @Test + public void testGetMetricsValue() { + for (TopicMessageType type : TopicMessageType.values()) { + String expected = type.getValue().toLowerCase(); + String actual = type.getMetricsValue(); + assertEquals(expected, actual); + } + } + + @Test + public void testParseFromMessageProperty() { + Map properties = new HashMap<>(); + + // TRANSACTION + properties.put(MessageConst.PROPERTY_TRANSACTION_PREPARED, "true"); + Assert.assertEquals(TopicMessageType.TRANSACTION, TopicMessageType.parseFromMessageProperty(properties)); + + // DELAY + properties.clear(); + properties.put(MessageConst.PROPERTY_DELAY_TIME_LEVEL, "3"); + Assert.assertEquals(TopicMessageType.DELAY, TopicMessageType.parseFromMessageProperty(properties)); + + properties.clear(); + properties.put(MessageConst.PROPERTY_TIMER_DELIVER_MS, System.currentTimeMillis() + 10000 + ""); + Assert.assertEquals(TopicMessageType.DELAY, TopicMessageType.parseFromMessageProperty(properties)); + + properties.clear(); + properties.put(MessageConst.PROPERTY_TIMER_DELAY_SEC, 10 + ""); + Assert.assertEquals(TopicMessageType.DELAY, TopicMessageType.parseFromMessageProperty(properties)); + + properties.clear(); + properties.put(MessageConst.PROPERTY_TIMER_DELAY_MS, 10000 + ""); + Assert.assertEquals(TopicMessageType.DELAY, TopicMessageType.parseFromMessageProperty(properties)); + + // FIFO + properties.clear(); + properties.put(MessageConst.PROPERTY_SHARDING_KEY, "sharding_key"); + Assert.assertEquals(TopicMessageType.FIFO, TopicMessageType.parseFromMessageProperty(properties)); + + // NORMAL + properties.clear(); + Assert.assertEquals(TopicMessageType.NORMAL, TopicMessageType.parseFromMessageProperty(properties)); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/chain/HandlerChainTest.java b/common/src/test/java/org/apache/rocketmq/common/chain/HandlerChainTest.java new file mode 100644 index 0000000..3a8499e --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/chain/HandlerChainTest.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.common.chain; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class HandlerChainTest { + + private HandlerChain handlerChain; + private Handler handler1; + private Handler handler2; + + @Before + public void setUp() { + handlerChain = HandlerChain.create(); + handler1 = (t, chain) -> "Handler1"; + handler2 = (t, chain) -> null; + } + + @Test + public void testHandle_withEmptyChain() { + handlerChain.addNext(handler1); + handlerChain.handle(1); + assertNull("Expected null since the handler chain is empty", handlerChain.handle(2)); + } + + @Test + public void testHandle_withNonEmptyChain() { + handlerChain.addNext(handler1); + + String result = handlerChain.handle(1); + + assertEquals("Handler1", result); + } + + @Test + public void testHandle_withMultipleHandlers() { + handlerChain.addNext(handler1); + handlerChain.addNext(handler2); + + String result1 = handlerChain.handle(1); + String result2 = handlerChain.handle(2); + + assertEquals("Handler1", result1); + assertNull("Expected null since there are no more handlers", result2); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/coldctr/AccAndTimeStampTest.java b/common/src/test/java/org/apache/rocketmq/common/coldctr/AccAndTimeStampTest.java new file mode 100644 index 0000000..01bb4ae --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/coldctr/AccAndTimeStampTest.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.coldctr; + +import java.util.concurrent.atomic.AtomicLong; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class AccAndTimeStampTest { + + private AccAndTimeStamp accAndTimeStamp; + + @Before + public void setUp() { + accAndTimeStamp = new AccAndTimeStamp(new AtomicLong()); + } + + @Test + public void testInitialValues() { + assertEquals("Cold accumulator should be initialized to 0", 0, accAndTimeStamp.getColdAcc().get()); + assertTrue("Last cold read time should be initialized to current time", accAndTimeStamp.getLastColdReadTimeMills() >= System.currentTimeMillis() - 1000); + assertTrue("Create time should be initialized to current time", accAndTimeStamp.getCreateTimeMills() >= System.currentTimeMillis() - 1000); + } + + @Test + public void testSetColdAcc() { + AtomicLong newColdAcc = new AtomicLong(100L); + accAndTimeStamp.setColdAcc(newColdAcc); + assertEquals("Cold accumulator should be set to new value", newColdAcc, accAndTimeStamp.getColdAcc()); + } + + @Test + public void testSetLastColdReadTimeMills() { + long newLastColdReadTimeMills = System.currentTimeMillis() + 1000; + accAndTimeStamp.setLastColdReadTimeMills(newLastColdReadTimeMills); + assertEquals("Last cold read time should be set to new value", newLastColdReadTimeMills, accAndTimeStamp.getLastColdReadTimeMills().longValue()); + } + + @Test + public void testSetCreateTimeMills() { + long newCreateTimeMills = System.currentTimeMillis() + 2000; + accAndTimeStamp.setCreateTimeMills(newCreateTimeMills); + assertEquals("Create time should be set to new value", newCreateTimeMills, accAndTimeStamp.getCreateTimeMills().longValue()); + } + + @Test + public void testToStringContainsCorrectInformation() { + String toStringOutput = accAndTimeStamp.toString(); + assertTrue("ToString should contain cold accumulator value", toStringOutput.contains("coldAcc=" + accAndTimeStamp.getColdAcc())); + assertTrue("ToString should contain last cold read time", toStringOutput.contains("lastColdReadTimeMills=" + accAndTimeStamp.getLastColdReadTimeMills())); + assertTrue("ToString should contain create time", toStringOutput.contains("createTimeMills=" + accAndTimeStamp.getCreateTimeMills())); + } +} 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 0000000..8110cfb --- /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/compression/CompressionTypeTest.java b/common/src/test/java/org/apache/rocketmq/common/compression/CompressionTypeTest.java new file mode 100644 index 0000000..f9586bd --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/compression/CompressionTypeTest.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.compression; + +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +public class CompressionTypeTest { + + @Test + public void testCompressionTypeValues() { + assertEquals(1, CompressionType.LZ4.getValue()); + assertEquals(2, CompressionType.ZSTD.getValue()); + assertEquals(3, CompressionType.ZLIB.getValue()); + } + + @Test + public void testCompressionTypeOf() { + assertEquals(CompressionType.LZ4, CompressionType.of("LZ4")); + assertEquals(CompressionType.ZSTD, CompressionType.of("ZSTD")); + assertEquals(CompressionType.ZLIB, CompressionType.of("ZLIB")); + assertThrows(RuntimeException.class, () -> CompressionType.of("UNKNOWN")); + } + + @Test + public void testCompressionTypeFindByValue() { + assertEquals(CompressionType.LZ4, CompressionType.findByValue(1)); + assertEquals(CompressionType.ZSTD, CompressionType.findByValue(2)); + assertEquals(CompressionType.ZLIB, CompressionType.findByValue(3)); + assertEquals(CompressionType.ZLIB, CompressionType.findByValue(0)); + assertThrows(RuntimeException.class, () -> CompressionType.findByValue(99)); + } + + @Test + public void testCompressionFlag() { + assertEquals(MessageSysFlag.COMPRESSION_LZ4_TYPE, CompressionType.LZ4.getCompressionFlag()); + assertEquals(MessageSysFlag.COMPRESSION_ZSTD_TYPE, CompressionType.ZSTD.getCompressionFlag()); + assertEquals(MessageSysFlag.COMPRESSION_ZLIB_TYPE, CompressionType.ZLIB.getCompressionFlag()); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/compression/CompressorFactoryTest.java b/common/src/test/java/org/apache/rocketmq/common/compression/CompressorFactoryTest.java new file mode 100644 index 0000000..e150fb2 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/compression/CompressorFactoryTest.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 org.junit.Assert; +import org.junit.Test; + +public class CompressorFactoryTest { + + @Test + public void testGetCompressor_ReturnsNonNull() { + for (CompressionType type : CompressionType.values()) { + Compressor compressor = CompressorFactory.getCompressor(type); + Assert.assertNotNull("Compressor should not be null for type " + type, compressor); + } + } + + @Test + public void testGetCompressor_ReturnsCorrectType() { + for (CompressionType type : CompressionType.values()) { + Compressor compressor = CompressorFactory.getCompressor(type); + Assert.assertTrue("Compressor type mismatch for " + type, + compressor instanceof Lz4Compressor && type == CompressionType.LZ4 || + compressor instanceof ZstdCompressor && type == CompressionType.ZSTD || + compressor instanceof ZlibCompressor && type == CompressionType.ZLIB); + } + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/compression/Lz4CompressorTest.java b/common/src/test/java/org/apache/rocketmq/common/compression/Lz4CompressorTest.java new file mode 100644 index 0000000..ca59025 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/compression/Lz4CompressorTest.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.compression; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import org.junit.Test; + +public class Lz4CompressorTest { + + private static final String TEST_STRING = "The quick brown fox jumps over the lazy dog"; + + @Test + public void testCompressAndDecompress() throws Exception { + byte[] originalData = TEST_STRING.getBytes(); + Compressor compressor = new Lz4Compressor(); + byte[] compressedData = compressor.compress(originalData, 1); + assertTrue("Compressed data should be bigger than original", compressedData.length > originalData.length); + + byte[] decompressedData = compressor.decompress(compressedData); + assertArrayEquals("Decompressed data should match original data", originalData, decompressedData); + } + + @Test + public void testCompressWithIOException() throws Exception { + byte[] originalData = new byte[] {1, 2, 3}; + Compressor compressor = new Lz4Compressor(); + compressor.compress(originalData, 1); + } + + @Test(expected = IOException.class) + public void testDecompressWithIOException() throws Exception { + byte[] compressedData = new byte[] {1, 2, 3}; + Compressor compressor = new Lz4Compressor(); + compressor.decompress(compressedData); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/compression/ZlibCompressorTest.java b/common/src/test/java/org/apache/rocketmq/common/compression/ZlibCompressorTest.java new file mode 100644 index 0000000..f46ac7c --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/compression/ZlibCompressorTest.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.compression; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import org.junit.Test; + +public class ZlibCompressorTest { + + private static final String TEST_STRING = "The quick brown fox jumps over the lazy dog"; + + @Test + public void testCompressionAndDecompression() throws Exception { + byte[] originalData = TEST_STRING.getBytes(); + ZlibCompressor compressor = new ZlibCompressor(); + byte[] compressedData = compressor.compress(originalData, 0); + assertTrue("Compressed data should be bigger than original", compressedData.length > originalData.length); + + byte[] decompressedData = compressor.decompress(compressedData); + assertArrayEquals("Decompressed data should match original", originalData, decompressedData); + } + + @Test + public void testCompressionFailureWithInvalidData() throws Exception { + byte[] originalData = new byte[] {0, 1, 2, 3, 4}; + ZlibCompressor compressor = new ZlibCompressor(); + compressor.compress(originalData, 0); + } + + @Test(expected = IOException.class) + public void testDecompressionFailureWithInvalidData() throws Exception { + byte[] compressedData = new byte[] {0, 1, 2, 3, 4}; + ZlibCompressor compressor = new ZlibCompressor(); + compressor.decompress(compressedData); // Invalid compressed data + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/compression/ZstdCompressorTest.java b/common/src/test/java/org/apache/rocketmq/common/compression/ZstdCompressorTest.java new file mode 100644 index 0000000..574e128 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/compression/ZstdCompressorTest.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.compression; + +import java.io.IOException; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertTrue; + +public class ZstdCompressorTest { + + @Test + public void testCompressAndDecompress() throws IOException { + byte[] originalData = "RocketMQ is awesome!".getBytes(); + ZstdCompressor compressor = new ZstdCompressor(); + byte[] compressedData = compressor.compress(originalData, 1); + assertTrue("Compressed data should be bigger than original", compressedData.length > originalData.length); + + byte[] decompressedData = compressor.decompress(compressedData); + assertArrayEquals("Decompressed data should match original data", originalData, decompressedData); + } + + @Test + public void testCompressWithInvalidData() throws IOException { + byte[] invalidData = new byte[] {-1, -1, -1, -1}; + ZstdCompressor compressor = new ZstdCompressor(); + compressor.compress(invalidData, 1); + } + + @Test(expected = IOException.class) + public void testDecompressWithInvalidData() throws IOException { + byte[] invalidData = new byte[] {-1, -1, -1, -1}; + ZstdCompressor compressor = new ZstdCompressor(); + compressor.decompress(invalidData); + } + + @Test + public void testCompressAndDecompressEmptyString() throws IOException { + byte[] originalData = "".getBytes(); + ZstdCompressor compressor = new ZstdCompressor(); + byte[] compressedData = compressor.compress(originalData, 1); + assertTrue("Compressed data for empty string should not be empty", compressedData.length > 0); + + byte[] decompressedData = compressor.decompress(compressedData); + assertArrayEquals("Decompressed data for empty string should match original", originalData, decompressedData); + } + + @Test + public void testCompressAndDecompressLargeData() throws IOException { + StringBuilder largeStringBuilder = new StringBuilder(); + for (int i = 0; i < 10000; i++) { + largeStringBuilder.append("RocketMQ is awesome! "); + } + byte[] originalData = largeStringBuilder.toString().getBytes(); + + ZstdCompressor compressor = new ZstdCompressor(); + byte[] compressedData = compressor.compress(originalData, 1); + assertTrue("Compressed data for large data should be smaller than original", compressedData.length < originalData.length); + + byte[] decompressedData = compressor.decompress(compressedData); + assertArrayEquals("Decompressed data for large data should match original", originalData, decompressedData); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/config/ConfigHelperTest.java b/common/src/test/java/org/apache/rocketmq/common/config/ConfigHelperTest.java new file mode 100644 index 0000000..1fcc967 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/config/ConfigHelperTest.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.common.config; + +import org.junit.Test; + +public class ConfigHelperTest { + + @Test + public void testGetDBLogDir() { + // Should not raise exception. + ConfigHelper.getDBLogDir(); + } + +} diff --git a/common/src/test/java/org/apache/rocketmq/common/consumer/ReceiptHandleTest.java b/common/src/test/java/org/apache/rocketmq/common/consumer/ReceiptHandleTest.java new file mode 100644 index 0000000..5474181 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/consumer/ReceiptHandleTest.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.common.consumer; + +import org.apache.rocketmq.common.KeyBuilder; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class ReceiptHandleTest { + + @Test + public void testEncodeAndDecode() { + long startOffset = 1000L; + long retrieveTime = System.currentTimeMillis(); + long invisibleTime = 1000L; + int reviveQueueId = 1; + String topicType = "NORMAL"; + String brokerName = "BrokerA"; + int queueId = 2; + long offset = 2000L; + long commitLogOffset = 3000L; + ReceiptHandle receiptHandle = ReceiptHandle.builder() + .startOffset(startOffset) + .retrieveTime(retrieveTime) + .invisibleTime(invisibleTime) + .reviveQueueId(reviveQueueId) + .topicType(topicType) + .brokerName(brokerName) + .queueId(queueId) + .offset(offset) + .commitLogOffset(commitLogOffset) + .build(); + + String encoded = receiptHandle.encode(); + ReceiptHandle decoded = ReceiptHandle.decode(encoded); + + assertEquals(receiptHandle.getStartOffset(), decoded.getStartOffset()); + assertEquals(receiptHandle.getRetrieveTime(), decoded.getRetrieveTime()); + assertEquals(receiptHandle.getInvisibleTime(), decoded.getInvisibleTime()); + assertEquals(receiptHandle.getReviveQueueId(), decoded.getReviveQueueId()); + assertEquals(receiptHandle.getTopicType(), decoded.getTopicType()); + assertEquals(receiptHandle.getBrokerName(), decoded.getBrokerName()); + assertEquals(receiptHandle.getQueueId(), decoded.getQueueId()); + assertEquals(receiptHandle.getOffset(), decoded.getOffset()); + assertEquals(receiptHandle.getCommitLogOffset(), decoded.getCommitLogOffset()); + } + + @Test(expected = IllegalArgumentException.class) + public void testDecodeWithInvalidString() { + String invalidReceiptHandle = "invalid_data"; + + ReceiptHandle.decode(invalidReceiptHandle); + } + + @Test + public void testIsExpired() { + long startOffset = 1000L; + long retrieveTime = System.currentTimeMillis(); + long invisibleTime = 1000L; + int reviveQueueId = 1; + String topicType = "NORMAL"; + String brokerName = "BrokerA"; + int queueId = 2; + long offset = 2000L; + long commitLogOffset = 3000L; + long pastTime = System.currentTimeMillis() - 1000L; + ReceiptHandle receiptHandle = new ReceiptHandle(startOffset, retrieveTime, invisibleTime, pastTime, reviveQueueId, topicType, brokerName, queueId, offset, commitLogOffset, ""); + + boolean isExpired = receiptHandle.isExpired(); + + assertTrue(isExpired); + } + + @Test + public void testGetRealTopic() { + // Arrange + String topic = "TestTopic"; + String groupName = "TestGroup"; + ReceiptHandle receiptHandle = ReceiptHandle.builder() + .topicType(ReceiptHandle.RETRY_TOPIC) + .build(); + + String realTopic = receiptHandle.getRealTopic(topic, groupName); + + assertEquals(KeyBuilder.buildPopRetryTopicV1(topic, groupName), realTopic); + } +} 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 0000000..22e6f3c --- /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 new file mode 100644 index 0000000..39bfbf5 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/message/MessageDecoderTest.java @@ -0,0 +1,431 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.junit.Test; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +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 { + + @Test + public void testDecodeProperties() { + 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(0); + 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"); + + { + 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); + } 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 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 new file mode 100644 index 0000000..c950970 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/message/MessageTest.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.message; + +import org.junit.Assert; +import org.junit.Test; + +import static org.apache.rocketmq.common.message.MessageConst.PROPERTY_TRACE_SWITCH; + +public class MessageTest { + @Test(expected = RuntimeException.class) + public void putUserPropertyWithRuntimeException() throws Exception { + Message m = new Message(); + + m.putUserProperty(PROPERTY_TRACE_SWITCH, ""); + } + + @Test(expected = IllegalArgumentException.class) + public void putUserNullValuePropertyWithException() throws Exception { + Message m = new Message(); + + m.putUserProperty("prop1", null); + } + + @Test(expected = IllegalArgumentException.class) + public void putUserEmptyValuePropertyWithException() throws Exception { + Message m = new Message(); + + m.putUserProperty("prop1", " "); + } + + @Test(expected = IllegalArgumentException.class) + public void putUserNullNamePropertyWithException() throws Exception { + Message m = new Message(); + + m.putUserProperty(null, "val1"); + } + + @Test(expected = IllegalArgumentException.class) + public void putUserEmptyNamePropertyWithException() throws Exception { + Message m = new Message(); + + m.putUserProperty(" ", "val1"); + } + + @Test + public void putUserProperty() throws Exception { + Message m = new Message(); + + m.putUserProperty("prop1", "val1"); + Assert.assertEquals("val1", m.getUserProperty("prop1")); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/producer/RecallMessageHandleTest.java b/common/src/test/java/org/apache/rocketmq/common/producer/RecallMessageHandleTest.java new file mode 100644 index 0000000..5660822 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/producer/RecallMessageHandleTest.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.producer; + +import org.apache.commons.codec.DecoderException; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.junit.Assert; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public class RecallMessageHandleTest { + @Test + public void testHandleInvalid() { + Assert.assertThrows(DecoderException.class, () -> { + RecallMessageHandle.decodeHandle(""); + }); + Assert.assertThrows(DecoderException.class, () -> { + RecallMessageHandle.decodeHandle(null); + }); + + Assert.assertThrows(DecoderException.class, () -> { + String invalidHandle = Base64.getUrlEncoder().encodeToString("v1 a b c".getBytes(StandardCharsets.UTF_8)); + RecallMessageHandle.decodeHandle(invalidHandle); + }); + Assert.assertThrows(DecoderException.class, () -> { + String invalidHandle = Base64.getUrlEncoder().encodeToString("v2 a b c d".getBytes(StandardCharsets.UTF_8)); + RecallMessageHandle.decodeHandle(invalidHandle); + }); + Assert.assertThrows(DecoderException.class, () -> { + String invalidHandle = "v1 a b c d"; + RecallMessageHandle.decodeHandle(invalidHandle); + }); + } + + @Test + public void testEncodeAndDecodeV1() throws DecoderException { + String topic = "topic"; + String brokerName = "broker-0"; + String timestampStr = String.valueOf(System.currentTimeMillis()); + String messageId = MessageClientIDSetter.createUniqID(); + String handle = RecallMessageHandle.HandleV1.buildHandle(topic, brokerName, timestampStr, messageId); + RecallMessageHandle handleEntity = RecallMessageHandle.decodeHandle(handle); + Assert.assertTrue(handleEntity instanceof RecallMessageHandle.HandleV1); + RecallMessageHandle.HandleV1 handleV1 = (RecallMessageHandle.HandleV1) handleEntity; + Assert.assertEquals(handleV1.getVersion(), "v1"); + Assert.assertEquals(handleV1.getTopic(), topic); + Assert.assertEquals(handleV1.getBrokerName(), brokerName); + Assert.assertEquals(handleV1.getTimestampStr(), timestampStr); + Assert.assertEquals(handleV1.getMessageId(), messageId); + } +} 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 0000000..b02fb60 --- /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 0000000..8590d56 --- /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 0000000..60e1812 --- /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 0000000..65954fa --- /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 0000000..778c6f2 --- /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 0000000..7320131 --- /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/IPAddressUtilsTest.java b/common/src/test/java/org/apache/rocketmq/common/utils/IPAddressUtilsTest.java new file mode 100644 index 0000000..404250e --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/utils/IPAddressUtilsTest.java @@ -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. + */ +package org.apache.rocketmq.common.utils; + +import org.junit.Test; + +public class IPAddressUtilsTest { + + @Test + public void isIPInRange() { + + // IPv4 test + String ipv4Address = "192.168.1.10"; + String ipv4Cidr = "192.168.1.0/24"; + assert IPAddressUtils.isIPInRange(ipv4Address, ipv4Cidr); + + ipv4Address = "192.168.2.10"; + assert !IPAddressUtils.isIPInRange(ipv4Address, ipv4Cidr); + + // IPv6 test + String ipv6Address = "2001:0db8:85a3:0000:0000:8a2e:0370:7334"; + String ipv6Cidr = "2001:0db8:85a3::/48"; + assert IPAddressUtils.isIPInRange(ipv6Address, ipv6Cidr); + } + + @Test + public void isValidCidr() { + String ipv4Cidr = "192.168.1.0/24"; + String ipv6Cidr = "2001:0db8:1234:5678::/64"; + String invalidCidr = "192.168.1.0"; + + assert IPAddressUtils.isValidCidr(ipv4Cidr); + assert IPAddressUtils.isValidCidr(ipv6Cidr); + assert !IPAddressUtils.isValidCidr(invalidCidr); + } + + @Test + public void isValidIp() { + String ipv4 = "192.168.1.0"; + String ipv6 = "2001:0db8:85a3:0000:0000:8a2e:0370:7334"; + String invalidIp = "192.168.1.256"; + String ipv4Cidr = "192.168.1.0/24"; + + assert IPAddressUtils.isValidIp(ipv4); + assert IPAddressUtils.isValidIp(ipv6); + assert !IPAddressUtils.isValidIp(invalidIp); + assert !IPAddressUtils.isValidIp(ipv4Cidr); + } + + @Test + public void isValidIPOrCidr() { + String ipv4 = "192.168.1.0"; + String ipv6 = "2001:0db8:85a3:0000:0000:8a2e:0370:7334"; + String ipv4Cidr = "192.168.1.0/24"; + String ipv6Cidr = "2001:0db8:1234:5678::/64"; + assert IPAddressUtils.isValidIPOrCidr(ipv4); + assert IPAddressUtils.isValidIPOrCidr(ipv6); + assert IPAddressUtils.isValidIPOrCidr(ipv4Cidr); + assert IPAddressUtils.isValidIPOrCidr(ipv6Cidr); + } +} \ No newline at end of file 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 0000000..38cffdb --- /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 0000000..8695d52 --- /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 0000000..059d7c2 --- /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 0000000..76d17af --- /dev/null +++ b/container/pom.xml @@ -0,0 +1,39 @@ + + + + org.apache.rocketmq + rocketmq-all + 5.3.4-SNAPSHOT + + + 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 0000000..fe126af --- /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 0000000..b8b7f7e --- /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, null); + + 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 0000000..03b4b26 --- /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 0000000..80dd6cc --- /dev/null +++ b/container/src/main/java/org/apache/rocketmq/container/BrokerContainerProcessor.java @@ -0,0 +1,310 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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); + 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 0000000..f909e62 --- /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 0000000..90c9122 --- /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 0000000..d3cdc05 --- /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 0000000..616188e --- /dev/null +++ b/container/src/main/java/org/apache/rocketmq/container/InnerBrokerController.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.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.RemotingServer; +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() { + RemotingServer remotingServer = this.brokerContainer.getRemotingServer().newRemotingServer(brokerConfig.getListenPort()); + RemotingServer fastRemotingServer = this.brokerContainer.getRemotingServer().newRemotingServer(brokerConfig.getListenPort() - 2); + + setRemotingServer(remotingServer); + setFastRemotingServer(fastRemotingServer); + } + + @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 (getRemotingServer() != null) { + this.brokerContainer.getRemotingServer().removeRemotingServer(brokerConfig.getListenPort()); + } + + if (getFastRemotingServer() != 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 0000000..a7901bc --- /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 0000000..bf51819 --- /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 0000000..1b9ef6d --- /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 0000000..e02d9ac --- /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/BrokerPreOnlineServiceTest.java b/container/src/test/java/org/apache/rocketmq/container/BrokerPreOnlineServiceTest.java new file mode 100644 index 0000000..6a46ed1 --- /dev/null +++ b/container/src/test/java/org/apache/rocketmq/container/BrokerPreOnlineServiceTest.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.container; + +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.BrokerSyncInfo; +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 java.lang.reflect.Field; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class BrokerPreOnlineServiceTest { + @Mock + private BrokerContainer brokerContainer; + + private InnerBrokerController innerBrokerController; + + @Mock + private BrokerOuterAPI brokerOuterAPI; + + public void init(final long brokerId) throws Exception { + when(brokerContainer.getBrokerOuterAPI()).thenReturn(brokerOuterAPI); + + BrokerMemberGroup brokerMemberGroup1 = new BrokerMemberGroup(); + Map brokerAddrMap = new HashMap<>(); + brokerAddrMap.put(0L, "127.0.0.1:10911"); + brokerMemberGroup1.setBrokerAddrs(brokerAddrMap); + + BrokerMemberGroup brokerMemberGroup2 = new BrokerMemberGroup(); + brokerMemberGroup2.setBrokerAddrs(new HashMap<>()); + when(brokerOuterAPI.syncBrokerMemberGroup(anyString(), anyString(), anyBoolean())) + .thenReturn(brokerMemberGroup1) + .thenReturn(brokerMemberGroup2); + BrokerSyncInfo brokerSyncInfo = mock(BrokerSyncInfo.class); + when(brokerOuterAPI.retrieveBrokerHaInfo(anyString())).thenReturn(brokerSyncInfo); + + DefaultMessageStore defaultMessageStore = mock(DefaultMessageStore.class); + when(defaultMessageStore.getMessageStoreConfig()).thenReturn(new MessageStoreConfig()); + BrokerConfig brokerConfig = new BrokerConfig(); + brokerConfig.setBrokerId(brokerId); + when(defaultMessageStore.getBrokerConfig()).thenReturn(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(0); + BrokerPreOnlineService brokerPreOnlineService = new BrokerPreOnlineService(innerBrokerController); + + brokerPreOnlineService.start(); + + await().atMost(Duration.ofSeconds(30)).until(() -> !innerBrokerController.isIsolated()); + } + + @Test + public void testMasterOnlineNormal() throws Exception { + init(0); + BrokerPreOnlineService brokerPreOnlineService = new BrokerPreOnlineService(innerBrokerController); + brokerPreOnlineService.start(); + TimeUnit.SECONDS.sleep(1); + brokerPreOnlineService.shutdown(); + await().atMost(Duration.ofSeconds(30)).until(brokerPreOnlineService::isStopped); + } + + @Test + public void testSlaveOnlineNormal() throws Exception { + init(1); + BrokerPreOnlineService brokerPreOnlineService = new BrokerPreOnlineService(innerBrokerController); + brokerPreOnlineService.start(); + TimeUnit.SECONDS.sleep(1); + brokerPreOnlineService.shutdown(); + await().atMost(Duration.ofSeconds(30)).until(brokerPreOnlineService::isStopped); + } + + @Test + public void testGetServiceName() throws Exception { + init(1); + BrokerPreOnlineService brokerPreOnlineService = new BrokerPreOnlineService(innerBrokerController); + assertEquals(BrokerPreOnlineService.class.getSimpleName(), brokerPreOnlineService.getServiceName()); + } +} 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 0000000..8695d52 --- /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 0000000..73c2cf3 --- /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 0000000..ff9a293 --- /dev/null +++ b/controller/pom.xml @@ -0,0 +1,74 @@ + + + + + rocketmq-all + org.apache.rocketmq + 5.3.4-SNAPSHOT + + 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 0000000..dea393f --- /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 0000000..d22d0b6 --- /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 0000000..cda6130 --- /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 0000000..6dad631 --- /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 0000000..275058d --- /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 0000000..d087f0d --- /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 0000000..da3b3ed --- /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 0000000..31fa476 --- /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 0000000..afdb270 --- /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 0000000..d8c6a2f --- /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 0000000..3421010 --- /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; + + // use 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("Controller leader append initial log failed, try {} 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 0000000..70c65c0 --- /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 0000000..e40a634 --- /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 0000000..0242038 --- /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 0000000..c9c470c --- /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 0000000..6af44b7 --- /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 0000000..bb8c9f5 --- /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 0000000..6992fa1 --- /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 0000000..d661d73 --- /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 0000000..c61e792 --- /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 0000000..8a31393 --- /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 0000000..b5358c7 --- /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 0000000..2b4cefb --- /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 0000000..ead4895 --- /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 0000000..7f1085c --- /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 0000000..8fc0495 --- /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 0000000..187f3ba --- /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 0000000..05d742f --- /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 ? 1 : 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 0000000..d981ff4 --- /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 ? 1 : 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 0000000..1623a05 --- /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 0000000..4920432 --- /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 0000000..086058d --- /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 0000000..0b2bc13 --- /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 0000000..9f8e0d7 --- /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 0000000..93462af --- /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 0000000..489b51b --- /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 0000000..2424ee6 --- /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 0000000..1798a41 --- /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 0000000..79a3c9c --- /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 0000000..56d2242 --- /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 0000000..02c3496 --- /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 0000000..6eb8750 --- /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 0000000..45b4006 --- /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 0000000..0200819 --- /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 0000000..2713cf3 --- /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 0000000..5629da9 --- /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 0000000..3cf387a --- /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 0000000..5256522 --- /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 0000000..f77f49d --- /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 0000000..32e7859 --- /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 0000000..9d693e4 --- /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 0000000..bc9c50c --- /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 0000000..8695d52 --- /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 new file mode 100644 index 0000000..981b2c2 --- /dev/null +++ b/dev/merge_rocketmq_pr.py @@ -0,0 +1,451 @@ +#!/usr/bin/env python + +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# 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) +# +# This utility assumes you already have local a RocketMQ git folder and that you +# have added remotes corresponding to both (i) the github apache RocketMQ +# mirror and (ii) the apache git repo. + +import json +import os +import re +import subprocess +import sys +import urllib2 + +try: + import jira.client + JIRA_IMPORTED = True +except ImportError: + JIRA_IMPORTED = False + +# Location of your RocketMQ git development area +ROCKETMQ_HOME = os.environ.get("ROCKETMQ_HOME", os.getcwd()) +# Remote name which points to the Gihub site +PR_REMOTE_NAME = os.environ.get("PR_REMOTE_NAME", "apache-github") +# Remote name which points to Apache git +PUSH_REMOTE_NAME = os.environ.get("PUSH_REMOTE_NAME", "origin") +# ASF JIRA username +JIRA_USERNAME = os.environ.get("JIRA_USERNAME", "") +# ASF JIRA password +JIRA_PASSWORD = os.environ.get("JIRA_PASSWORD", "") +# OAuth key used for issuing requests against the GitHub API. If this is not defined, then requests +# will be unauthenticated. You should only need to configure this if you find yourself regularly +# exceeding your IP's unauthenticated request rate limit. You can create an OAuth key at +# https://github.com/settings/tokens. This script only requires the "public_repo" scope. +GITHUB_OAUTH_KEY = os.environ.get("GITHUB_OAUTH_KEY") + + +GITHUB_BASE = "https://github.com/apache/rocketmq/pull" +GITHUB_API_BASE = "https://api.github.com/repos/apache/rocketmq" +JIRA_BASE = "https://issues.apache.org/jira/browse" +JIRA_API_BASE = "https://issues.apache.org/jira" +# Prefix added to temporary branches +BRANCH_PREFIX = "PR_TOOL" +DEVELOP_BRANCH = "develop" + + +def get_json(url): + try: + request = urllib2.Request(url) + if GITHUB_OAUTH_KEY: + request.add_header('Authorization', 'token %s' % GITHUB_OAUTH_KEY) + return json.load(urllib2.urlopen(request)) + except urllib2.HTTPError as e: + if "X-RateLimit-Remaining" in e.headers and e.headers["X-RateLimit-Remaining"] == '0': + print("Exceeded the GitHub API rate limit; see the instructions in " + + "dev/merge_rocketmq_pr.py to configure an OAuth token for making authenticated " + + "GitHub requests.") + else: + print("Unable to fetch URL, exiting: %s" % url) + sys.exit(-1) + + +def fail(msg): + print(msg) + clean_up() + sys.exit(-1) + + +def run_cmd(cmd): + print(cmd) + if isinstance(cmd, list): + return subprocess.check_output(cmd) + else: + return subprocess.check_output(cmd.split(" ")) + + +def continue_maybe(prompt): + result = raw_input("\n%s (y/n): " % prompt) + if result.lower() != "y": + fail("Okay, exiting") + + +def clean_up(): + print("Restoring head pointer to %s" % original_head) + run_cmd("git checkout %s" % original_head) + + branches = run_cmd("git branch").replace(" ", "").split("\n") + + for branch in filter(lambda x: x.startswith(BRANCH_PREFIX), branches): + print("Deleting local branch %s" % branch) + run_cmd("git branch -D %s" % branch) + + +# merge the requested PR and return the merge hash +def merge_pr(pr_num, target_ref, title, body, pr_repo_desc): + pr_branch_name = "%s_MERGE_PR_%s" % (BRANCH_PREFIX, pr_num) + target_branch_name = "%s_MERGE_PR_%s_%s" % (BRANCH_PREFIX, pr_num, target_ref.upper()) + run_cmd("git fetch %s pull/%s/head:%s" % (PR_REMOTE_NAME, pr_num, pr_branch_name)) + run_cmd("git fetch %s %s:%s" % (PUSH_REMOTE_NAME, target_ref, target_branch_name)) + run_cmd("git checkout %s" % target_branch_name) + + had_conflicts = False + try: + run_cmd(['git', 'merge', pr_branch_name, '--squash']) + except Exception as e: + msg = "Error merging: %s\nWould you like to manually fix-up this merge?" % e + continue_maybe(msg) + msg = "Okay, please fix any conflicts and 'git add' conflicting files... Finished?" + continue_maybe(msg) + had_conflicts = True + + commit_authors = run_cmd(['git', 'log', 'HEAD..%s' % pr_branch_name, + '--pretty=format:%an <%ae>']).split("\n") + distinct_authors = sorted(set(commit_authors), + key=lambda x: commit_authors.count(x), reverse=True) + primary_author = raw_input( + "Enter primary author in the format of \"name \" [%s]: " % + distinct_authors[0]) + if primary_author == "": + primary_author = distinct_authors[0] + + commits = run_cmd(['git', 'log', 'HEAD..%s' % pr_branch_name, + '--pretty=format:%h [%an] %s']).split("\n\n") + + merge_message_flags = [] + + modified_title = raw_input("Modify commit log [%s]: " % title) + if modified_title == "": + modified_title = title + merge_message_flags += ["-m", modified_title] + + authors = "\n".join(["Author: %s" % a for a in distinct_authors]) + + merge_message_flags += ["-m", authors] + + if had_conflicts: + committer_name = run_cmd("git config --get user.name").strip() + committer_email = run_cmd("git config --get user.email").strip() + message = "This patch had conflicts when merged, resolved by\nCommitter: %s <%s>" % ( + committer_name, committer_email) + merge_message_flags += ["-m", message] + + # The string "Closes #%s" string is required for GitHub to correctly close the PR + merge_message_flags += ["-m", "Closes #%s from %s." % (pr_num, pr_repo_desc)] + + run_cmd(['git', 'commit', '--author="%s"' % primary_author] + merge_message_flags) + + continue_maybe("Merge complete (local ref %s). Push to %s?" % ( + target_branch_name, PUSH_REMOTE_NAME)) + + try: + run_cmd('git push %s %s:%s' % (PUSH_REMOTE_NAME, target_branch_name, target_ref)) + except Exception as e: + clean_up() + fail("Exception while pushing: %s" % e) + + merge_hash = run_cmd("git rev-parse %s" % target_branch_name)[:8] + clean_up() + print("Pull request #%s merged!" % pr_num) + print("Merge hash: %s" % merge_hash) + return merge_hash + + +def cherry_pick(pr_num, merge_hash, default_branch): + pick_ref = raw_input("Enter a branch name [%s]: " % default_branch) + if pick_ref == "": + pick_ref = default_branch + + pick_branch_name = "%s_PICK_PR_%s_%s" % (BRANCH_PREFIX, pr_num, pick_ref.upper()) + + run_cmd("git fetch %s %s:%s" % (PUSH_REMOTE_NAME, pick_ref, pick_branch_name)) + run_cmd("git checkout %s" % pick_branch_name) + + try: + run_cmd("git cherry-pick -sx %s" % merge_hash) + except Exception as e: + msg = "Error cherry-picking: %s\nWould you like to manually fix-up this merge?" % e + continue_maybe(msg) + msg = "Okay, please fix any conflicts and finish the cherry-pick. Finished?" + continue_maybe(msg) + + continue_maybe("Pick complete (local ref %s). Push to %s?" % ( + pick_branch_name, PUSH_REMOTE_NAME)) + + try: + run_cmd('git push %s %s:%s' % (PUSH_REMOTE_NAME, pick_branch_name, pick_ref)) + except Exception as e: + clean_up() + fail("Exception while pushing: %s" % e) + + pick_hash = run_cmd("git rev-parse %s" % pick_branch_name)[:8] + clean_up() + + print("Pull request #%s picked into %s!" % (pr_num, pick_ref)) + print("Pick hash: %s" % pick_hash) + return pick_ref + + +def fix_version_from_branch(branch, versions): + # Note: Assumes this is a sorted (newest->oldest) list of un-released versions + if branch == "master": + return versions[0] + else: + branch_ver = branch.replace("branch-", "") + return filter(lambda x: x.name.startswith(branch_ver), versions)[-1] + + +def resolve_jira_issue(merge_branches, comment, default_jira_id=""): + asf_jira = jira.client.JIRA({'server': JIRA_API_BASE}, + basic_auth=(JIRA_USERNAME, JIRA_PASSWORD)) + + jira_id = raw_input("Enter a JIRA id [%s]: " % default_jira_id) + if jira_id == "": + jira_id = default_jira_id + + try: + issue = asf_jira.issue(jira_id) + except Exception as e: + fail("ASF JIRA could not find %s\n%s" % (jira_id, e)) + + cur_status = issue.fields.status.name + cur_summary = issue.fields.summary + cur_assignee = issue.fields.assignee + if cur_assignee is None: + cur_assignee = "NOT ASSIGNED!!!" + else: + cur_assignee = cur_assignee.displayName + + if cur_status == "Resolved" or cur_status == "Closed": + fail("JIRA issue %s already has status '%s'" % (jira_id, cur_status)) + print("=== JIRA %s ===" % jira_id) + print("summary\t\t%s\nassignee\t%s\nstatus\t\t%s\nurl\t\t%s/%s\n" % + (cur_summary, cur_assignee, cur_status, JIRA_BASE, jira_id)) + + versions = asf_jira.project_versions("ROCKETMQ") + versions = sorted(versions, key=lambda x: x.name, reverse=True) + versions = filter(lambda x: x.raw['released'] is False, versions) + # Consider only x.y.z versions + versions = filter(lambda x: re.match('\d+\.\d+\.\d+', x.name), versions) + + default_fix_versions = map(lambda x: fix_version_from_branch(x, versions).name, merge_branches) + for v in default_fix_versions: + # Handles the case where we have forked a release branch but not yet made the release. + # In this case, if the PR is committed to the master branch and the release branch, we + # only consider the release branch to be the fix version. E.g. it is not valid to have + # both 1.1.0 and 1.0.0 as fix versions. + (major, minor, patch) = v.split(".") + if patch == "0": + previous = "%s.%s.%s" % (major, int(minor) - 1, 0) + if previous in default_fix_versions: + default_fix_versions = filter(lambda x: x != v, default_fix_versions) + default_fix_versions = ",".join(default_fix_versions) + + fix_versions = raw_input("Enter comma-separated fix version(s) [%s]: " % default_fix_versions) + if fix_versions == "": + fix_versions = default_fix_versions + fix_versions = fix_versions.replace(" ", "").split(",") + + def get_version_json(version_str): + return filter(lambda v: v.name == version_str, versions)[0].raw + + jira_fix_versions = map(lambda v: get_version_json(v), fix_versions) + + resolve = filter(lambda a: a['name'] == "Resolve Issue", asf_jira.transitions(jira_id))[0] + resolution = filter(lambda r: r.raw['name'] == "Fixed", asf_jira.resolutions())[0] + asf_jira.transition_issue( + jira_id, resolve["id"], fixVersions=jira_fix_versions, + comment=comment, resolution={'id': resolution.raw['id']}) + + print("Successfully resolved %s with fixVersions=%s!" % (jira_id, fix_versions)) + + +def resolve_jira_issues(title, merge_branches, comment): + jira_ids = re.findall("ROCKETMQ-[0-9]{4,5}", title) + + if len(jira_ids) == 0: + resolve_jira_issue(merge_branches, comment) + for jira_id in jira_ids: + resolve_jira_issue(merge_branches, comment, jira_id) + + +def standardize_jira_ref(text): + """ + Standardize the [ROCKETMQ-XXXXX] [MODULE] prefix + Converts "[ROCKETMQ-XXX][mllib] Issue", "[MLLib] ROCKETMQ-XXX. Issue" or "ROCKETMQ XXX [MLLIB]: Issue" to + "[ROCKETMQ-XXX][MLLIB] Issue" + """ + jira_refs = [] + components = [] + + # If the string is compliant, no need to process any further + if (re.search(r'^\[ROCKETMQ-[0-9]{3,6}\](\[[A-Z0-9_\s,]+\] )+\S+', text)): + return text + + # Extract JIRA ref(s): + pattern = re.compile(r'(ROCKETMQ[-\s]*[0-9]{3,6})+', re.IGNORECASE) + for ref in pattern.findall(text): + # Add brackets, replace spaces with a dash, & convert to uppercase + jira_refs.append('[' + re.sub(r'\s+', '-', ref.upper()) + ']') + text = text.replace(ref, '') + + # Extract rocketmq component(s): + # Look for alphanumeric chars, spaces, dashes, periods, and/or commas + pattern = re.compile(r'(\[[\w\s,-\.]+\])', re.IGNORECASE) + for component in pattern.findall(text): + components.append(component.upper()) + text = text.replace(component, '') + + # Cleanup any remaining symbols: + pattern = re.compile(r'^\W+(.*)', re.IGNORECASE) + if (pattern.search(text) is not None): + text = pattern.search(text).groups()[0] + + # Assemble full text (JIRA ref(s), module(s), remaining text) + clean_text = ''.join(jira_refs).strip() + ''.join(components).strip() + " " + text.strip() + + # Replace multiple spaces with a single space, e.g. if no jira refs and/or components were + # included + clean_text = re.sub(r'\s+', ' ', clean_text.strip()) + + return clean_text + + +def get_current_ref(): + ref = run_cmd("git rev-parse --abbrev-ref HEAD").strip() + if ref == 'HEAD': + # The current ref is a detached HEAD, so grab its SHA. + return run_cmd("git rev-parse HEAD").strip() + else: + return ref + + +def main(): + global original_head + + os.chdir(ROCKETMQ_HOME) + original_head = get_current_ref() + + latest_branch = DEVELOP_BRANCH + + pr_num = raw_input("Which pull request would you like to merge? (e.g. 34): ") + pr = get_json("%s/pulls/%s" % (GITHUB_API_BASE, pr_num)) + pr_events = get_json("%s/issues/%s/events" % (GITHUB_API_BASE, pr_num)) + + url = pr["url"] + + # Decide whether to use the modified title or not + modified_title = standardize_jira_ref(pr["title"]) + if modified_title != pr["title"]: + print("I've re-written the title as follows to match the standard format:") + print("Original: %s" % pr["title"]) + print("Modified: %s" % modified_title) + result = raw_input("Would you like to use the modified title? (y/n): ") + if result.lower() == "y": + title = modified_title + print("Using modified title:") + else: + title = pr["title"] + print("Using original title:") + print(title) + else: + title = pr["title"] + + body = pr["body"] + target_ref = pr["base"]["ref"] + user_login = pr["user"]["login"] + base_ref = pr["head"]["ref"] + pr_repo_desc = "%s/%s" % (user_login, base_ref) + + # Merged pull requests don't appear as merged in the GitHub API; + # Instead, they're closed by asfgit. + merge_commits = \ + [e for e in pr_events if e["actor"]["login"] == "asfgit" and e["event"] == "closed"] + + if merge_commits: + merge_hash = merge_commits[0]["commit_id"] + message = get_json("%s/commits/%s" % (GITHUB_API_BASE, merge_hash))["commit"]["message"] + + print("Pull request %s has already been merged, assuming you want to backport" % pr_num) + commit_is_downloaded = run_cmd(['git', 'rev-parse', '--quiet', '--verify', + "%s^{commit}" % merge_hash]).strip() != "" + if not commit_is_downloaded: + fail("Couldn't find any merge commit for #%s, you may need to update HEAD." % pr_num) + + print("Found commit %s:\n%s" % (merge_hash, message)) + cherry_pick(pr_num, merge_hash, latest_branch) + sys.exit(0) + + if not bool(pr["mergeable"]): + msg = "Pull request %s is not mergeable in its current form.\n" % pr_num + \ + "Continue? (experts only!)" + continue_maybe(msg) + + print("\n=== Pull Request #%s ===" % pr_num) + print("title\t%s\nsource\t%s\ntarget\t%s\nurl\t%s" % + (title, pr_repo_desc, target_ref, url)) + continue_maybe("Proceed with merging pull request #%s?" % pr_num) + + merged_refs = [target_ref] + + merge_hash = merge_pr(pr_num, target_ref, title, body, pr_repo_desc) + + pick_prompt = "Would you like to pick %s into another branch?" % merge_hash + while raw_input("\n%s (y/n): " % pick_prompt).lower() == "y": + merged_refs = merged_refs + [cherry_pick(pr_num, merge_hash, latest_branch)] + + if JIRA_IMPORTED: + if JIRA_USERNAME and JIRA_PASSWORD: + continue_maybe("Would you like to update an associated JIRA?") + jira_comment = "Issue resolved by pull request %s\n[%s/%s]" % \ + (pr_num, GITHUB_BASE, pr_num) + resolve_jira_issues(title, merged_refs, jira_comment) + else: + print("JIRA_USERNAME and JIRA_PASSWORD not set") + print("Exiting without trying to close the associated JIRA.") + else: + print("Could not find jira-python library. Run 'sudo pip install jira' to install.") + print("Exiting without trying to close the associated JIRA.") + +if __name__ == "__main__": + import doctest + (failure_count, test_count) = doctest.testmod() + if failure_count: + exit(-1) + try: + main() + except: + clean_up() + raise diff --git a/distribution/LICENSE-BIN b/distribution/LICENSE-BIN new file mode 100644 index 0000000..bd431bf --- /dev/null +++ b/distribution/LICENSE-BIN @@ -0,0 +1,334 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "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 (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. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +------ +This product has a bundle logback, which is available under the EPL v1.0 License. +The source code of logback can be found at https://github.com/qos-ch/logback. + +Logback LICENSE +--------------- + +Logback: the reliable, generic, fast and flexible logging framework. +Copyright (C) 1999-2015, QOS.ch. All rights reserved. + +This program and the accompanying materials are dual-licensed under +either the terms of the Eclipse Public License v1.0 as published by +the Eclipse Foundation + + or (per the licensee's choosing) + +under the terms of the GNU Lesser General Public License version 2.1 +as published by the Free Software Foundation. + +------ +This product has a bundle slf4j, which is available under the MIT License. +The source code of slf4j can be found at https://github.com/qos-ch/slf4j. + + Copyright (c) 2004-2017 QOS.ch + All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------ +This product has a bundle fastjson, which is available under the ASL2 License. +The source code of fastjson can be found at https://github.com/alibaba/fastjson. + + Copyright 1999-2016 Alibaba Group Holding Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +------ +This product has a bundle javassist, which is available under the ASL2 License. +The source code of javassist can be found at https://github.com/jboss-javassist/javassist. + + Copyright (C) 1999- by Shigeru Chiba, All rights reserved. + + Javassist (JAVA programming ASSISTant) makes Java bytecode manipulation simple. + It is a class library for editing bytecodes in Java; it enables Java programs to define a new class + at runtime and to modify a class file when the JVM loads it. Unlike other similar bytecode editors, + Javassist provides two levels of API: source level and bytecode level. If the users use the source- level API, + they can edit a class file without knowledge of the specifications of the Java bytecode. + The whole API is designed with only the vocabulary of the Java language. + You can even specify inserted bytecode in the form of source text; Javassist compiles it on the fly. + On the other hand, the bytecode-level API allows the users to directly edit a class file as other editors. + + This software is distributed under the Mozilla Public License Version 1.1, + the GNU Lesser General Public License Version 2.1 or later, or the Apache License Version 2.0. + +------ +This product has a bundle jna, which is available under the ASL2 License. +The source code of jna can be found at https://github.com/java-native-access/jna. + + This copy of JNA is licensed under the + Apache (Software) License, version 2.0 ("the License"). + See the License for details about distribution rights, and the + specific rights regarding derivate works. + + You may obtain a copy of the License at: + + http://www.apache.org/licenses/ + + A copy is also included in the downloadable source code package + containing JNA, in file "AL2.0", under the same directory + as this file. +------ +This product has a bundle guava, which is available under the ASL2 License. +The source code of guava can be found at https://github.com/google/guava. + + Copyright (C) 2007 The Guava authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +------ +This product has a bundle OpenMessaging, which is available under the ASL2 License. +The source code of OpenMessaging can be found at https://github.com/openmessaging/openmessaging. + + Copyright (C) 2017 The OpenMessaging authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/distribution/NOTICE-BIN b/distribution/NOTICE-BIN new file mode 100644 index 0000000..145520b --- /dev/null +++ b/distribution/NOTICE-BIN @@ -0,0 +1,36 @@ +Apache RocketMQ +Copyright 2016-2022 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +------ +This product has a bundle netty: + The Netty Project + ================= + +Please visit the Netty web site for more information: + + * http://netty.io/ + +Copyright 2014 The Netty Project + +The Netty Project licenses this file to you under the Apache License, +version 2.0 (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at: + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. + +Also, please refer to each LICENSE..txt file, which is located in +the 'license' directory of the distribution file, for the license terms of the +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()) diff --git a/distribution/benchmark/batchproducer.sh b/distribution/benchmark/batchproducer.sh new file mode 100644 index 0000000..0729700 --- /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/consumer.sh b/distribution/benchmark/consumer.sh new file mode 100644 index 0000000..6f9cd3d --- /dev/null +++ b/distribution/benchmark/consumer.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.Consumer $@ & diff --git a/distribution/benchmark/producer.sh b/distribution/benchmark/producer.sh new file mode 100644 index 0000000..3116d11 --- /dev/null +++ b/distribution/benchmark/producer.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 -Dorg.apache.rocketmq.client.sendSmartMsg=true org.apache.rocketmq.example.benchmark.Producer $@ & diff --git a/distribution/benchmark/runclass.sh b/distribution/benchmark/runclass.sh new file mode 100644 index 0000000..885e222 --- /dev/null +++ b/distribution/benchmark/runclass.sh @@ -0,0 +1,72 @@ +#!/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 [ $# -lt 1 ]; +then + echo "USAGE: $0 classname opts" + exit 1 +fi + +BASE_DIR=$(dirname $0)/.. +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:${GC_LOG_DIR}/rmq_run_class_gc_%p_%t.log -XX:+PrintGCDetails" +JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow" +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 +fi + +JAVA="$JAVA_HOME/bin/java" + +$JAVA ${JAVA_OPT} $@ diff --git a/distribution/benchmark/shutdown.sh b/distribution/benchmark/shutdown.sh new file mode 100644 index 0000000..2af7ef7 --- /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 new file mode 100644 index 0000000..39afbe3 --- /dev/null +++ b/distribution/benchmark/tproducer.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.TransactionProducer $@ & diff --git a/distribution/bin/README.md b/distribution/bin/README.md new file mode 100644 index 0000000..efbb67d --- /dev/null +++ b/distribution/bin/README.md @@ -0,0 +1,29 @@ +### Operating system tuning +Before deploying broker servers, it's highly recommended to run **os.sh**, which is to optimize your operating system for better performance. + +## Notice +### os.sh should be executed only once with root permission. +### os.sh parameter settings are for reference purpose only. You can tune them according to your target host configurations. + + +### Start broker +* Unix platform + + `nohup sh mqbroker &` + +### Shutdown broker + sh mqshutdown broker + +### Start Nameserver +* Unix platform + + `nohup sh mqnamesrv &` + +### Shutdown Nameserver + sh mqshutdown namesrv + +### Update or create Topic + sh mqadmin updateTopic -b 127.0.0.1:10911 -t TopicA + +### Update or create subscription group + sh mqadmin updateSubGroup -b 127.0.0.1:10911 -g SubGroupA \ No newline at end of file diff --git a/distribution/bin/cachedog.sh b/distribution/bin/cachedog.sh new file mode 100644 index 0000000..9329fdb --- /dev/null +++ b/distribution/bin/cachedog.sh @@ -0,0 +1,42 @@ +#!/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. + +export PATH=$PATH:/sbin + +while true; do + nr_free_pages=`fgrep -A 10 Normal /proc/zoneinfo |grep nr_free_pages |awk -F ' ' '{print $2}'` + high=`fgrep -A 10 Normal /proc/zoneinfo |grep high |awk -F ' ' '{print $2}'` + + NOW_DATE=`date +%D` + NOW_TIME=`date +%T` + + if [ ${nr_free_pages} -le ${high} ]; then + sysctl -w vm.drop_caches=3 + nr_free_pages_new=`fgrep -A 10 Normal /proc/zoneinfo |grep nr_free_pages |awk -F ' ' '{print $2}'` + + printf "%s %s [CLEAN] nr_free_pages < high, clean cache. nr_free_pages=%s ====> nr_free_pages=%s\n" "${NOW_DATE}" "${NOW_TIME}" ${nr_free_pages} ${nr_free_pages_new} + + sysctl -w vm.drop_caches=1 + echo + echo + echo + else + printf "%s %s [NOTHING] nr_free_pages=%s high=%s\n" "${NOW_DATE}" "${NOW_TIME}" ${nr_free_pages} ${high} + fi + + sleep 1 +done diff --git a/distribution/bin/cleancache.sh b/distribution/bin/cleancache.sh new file mode 100644 index 0000000..9c6e9ab --- /dev/null +++ b/distribution/bin/cleancache.sh @@ -0,0 +1,20 @@ +#!/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. + +export PATH=$PATH:/sbin + +sysctl -w vm.drop_caches=3 diff --git a/distribution/bin/cleancache.v1.sh b/distribution/bin/cleancache.v1.sh new file mode 100644 index 0000000..b334841 --- /dev/null +++ b/distribution/bin/cleancache.v1.sh @@ -0,0 +1,40 @@ +#!/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. + +export PATH=$PATH:/sbin + +# +# GB +# +function changeFreeCache() +{ + EXTRA=$1 + MIN=$2 + sysctl -w vm.extra_free_kbytes=${EXTRA}000000 + sysctl -w vm.min_free_kbytes=${MIN}000000 +} + + +if [ $# -ne 1 ] +then + echo "Usage: $0 freecache(GB)" + echo "Example: $0 15" + exit +fi + +changeFreeCache 3 $1 +changeFreeCache 3 1 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 0000000..debddef --- /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 0000000..7aa52d5 --- /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 0000000..6633d3a --- /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 0000000..a349153 --- /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 0000000..a32ed61 --- /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 0000000..211a4ff --- /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 0000000..acdde71 --- /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 0000000..2b323e8 --- /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 new file mode 100644 index 0000000..489da8b --- /dev/null +++ b/distribution/bin/mqadmin @@ -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/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 new file mode 100644 index 0000000..a28facb --- /dev/null +++ b/distribution/bin/mqadmin.cmd @@ -0,0 +1,18 @@ +@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\tools.cmd" echo Please set the ROCKETMQ_HOME variable in your environment! & EXIT /B 1 +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/mqbroker b/distribution/bin/mqbroker new file mode 100644 index 0000000..35eb93c --- /dev/null +++ b/distribution/bin/mqbroker @@ -0,0 +1,78 @@ +#!/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 + +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 new file mode 100644 index 0000000..644e217 --- /dev/null +++ b/distribution/bin/mqbroker.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\runbroker.cmd" echo Please set the ROCKETMQ_HOME variable in your environment! & EXIT /B 1 + +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" +) \ No newline at end of file diff --git a/distribution/bin/mqbroker.numanode0 b/distribution/bin/mqbroker.numanode0 new file mode 100644 index 0000000..b7486a7 --- /dev/null +++ b/distribution/bin/mqbroker.numanode0 @@ -0,0 +1,47 @@ +#!/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 + +export RMQ_NUMA_NODE=0 + +sh ${ROCKETMQ_HOME}/bin/mqbroker $@ diff --git a/distribution/bin/mqbroker.numanode1 b/distribution/bin/mqbroker.numanode1 new file mode 100644 index 0000000..c301fed --- /dev/null +++ b/distribution/bin/mqbroker.numanode1 @@ -0,0 +1,47 @@ +#!/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 + +export RMQ_NUMA_NODE=1 + +sh ${ROCKETMQ_HOME}/bin/mqbroker $@ diff --git a/distribution/bin/mqbroker.numanode2 b/distribution/bin/mqbroker.numanode2 new file mode 100644 index 0000000..ea95304 --- /dev/null +++ b/distribution/bin/mqbroker.numanode2 @@ -0,0 +1,47 @@ +#!/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 + +export RMQ_NUMA_NODE=2 + +sh ${ROCKETMQ_HOME}/bin/mqbroker $@ diff --git a/distribution/bin/mqbroker.numanode3 b/distribution/bin/mqbroker.numanode3 new file mode 100644 index 0000000..25d3d1d --- /dev/null +++ b/distribution/bin/mqbroker.numanode3 @@ -0,0 +1,47 @@ +#!/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 + +export RMQ_NUMA_NODE=3 + +sh ${ROCKETMQ_HOME}/bin/mqbroker $@ diff --git a/distribution/bin/mqbrokercontainer b/distribution/bin/mqbrokercontainer new file mode 100644 index 0000000..0ce383f --- /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 0000000..5ac064d --- /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 0000000..95fae97 --- /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/mqnamesrv b/distribution/bin/mqnamesrv new file mode 100644 index 0000000..6741c7f --- /dev/null +++ b/distribution/bin/mqnamesrv @@ -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.namesrv.logback.xml org.apache.rocketmq.namesrv.NamesrvStartup $@ diff --git a/distribution/bin/mqnamesrv.cmd b/distribution/bin/mqnamesrv.cmd new file mode 100644 index 0000000..97219d8 --- /dev/null +++ b/distribution/bin/mqnamesrv.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.namesrv.logback.xml org.apache.rocketmq.namesrv.NamesrvStartup %* + +IF %ERRORLEVEL% EQU 0 ( + ECHO "Namesrv starts OK" +) \ No newline at end of file diff --git a/distribution/bin/mqproxy b/distribution/bin/mqproxy new file mode 100644 index 0000000..d6a8f3f --- /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 0000000..51f7b21 --- /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 new file mode 100644 index 0000000..f4b58e2 --- /dev/null +++ b/distribution/bin/mqshutdown @@ -0,0 +1,96 @@ +#!/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 + 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; + fi + + echo "The mqbroker(${pid}) is running..." + + kill ${pid} + + 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; + fi + + echo "The mqnamesrv(${pid}) is running..." + + kill ${pid} + + 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 "Usage: mqshutdown broker | namesrv | controller | proxy" +esac diff --git a/distribution/bin/mqshutdown.cmd b/distribution/bin/mqshutdown.cmd new file mode 100644 index 0000000..32fcc99 --- /dev/null +++ b/distribution/bin/mqshutdown.cmd @@ -0,0 +1,41 @@ +@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 "%JAVA_HOME%\bin\jps.exe" echo Please set the JAVA_HOME variable in your environment, We need java(x64)! & EXIT /B 1 + +setlocal + +set "PATH=%JAVA_HOME%\bin;%PATH%" + +if /I "%1" == "broker" ( + echo killing broker + for /f "tokens=1" %%i in ('jps -m ^| find "BrokerStartup"') do ( taskkill /F /PID %%i ) + echo Done! +) else if /I "%1" == "namesrv" ( + echo killing name server + + 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 or controller +) \ No newline at end of file diff --git a/distribution/bin/os.sh b/distribution/bin/os.sh new file mode 100644 index 0000000..1a8a6cd --- /dev/null +++ b/distribution/bin/os.sh @@ -0,0 +1,64 @@ +#!/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. + +export PATH=$PATH:/sbin + +# sudo sysctl -w vm.extra_free_kbytes=2000000 +# sudo sysctl -w vm.min_free_kbytes=1000000 +sudo sysctl -w vm.overcommit_memory=1 +sudo sysctl -w vm.drop_caches=1 +sudo sysctl -w vm.zone_reclaim_mode=0 +sudo sysctl -w vm.max_map_count=655360 +sudo sysctl -w vm.dirty_background_ratio=50 +sudo sysctl -w vm.dirty_ratio=50 +sudo sysctl -w vm.dirty_writeback_centisecs=360000 +sudo sysctl -w vm.page-cluster=3 +sudo sysctl -w vm.swappiness=1 + +echo 'ulimit -n 655350' >> /etc/profile +echo '* hard nofile 655350' >> /etc/security/limits.conf + +echo '* hard memlock unlimited' >> /etc/security/limits.conf +echo '* soft memlock unlimited' >> /etc/security/limits.conf + +DISK=`df -k | sort -n -r -k 2 | awk -F/ 'NR==1 {gsub(/[0-9].*/,"",$3); print $3}'` +[ "$DISK" = 'cciss' ] && DISK='cciss!c0d0' +echo 'deadline' > /sys/block/${DISK}/queue/scheduler + + +echo "---------------------------------------------------------------" +sysctl vm.extra_free_kbytes +sysctl vm.min_free_kbytes +sysctl vm.overcommit_memory +sysctl vm.drop_caches +sysctl vm.zone_reclaim_mode +sysctl vm.max_map_count +sysctl vm.dirty_background_ratio +sysctl vm.dirty_ratio +sysctl vm.dirty_writeback_centisecs +sysctl vm.page-cluster +sysctl vm.swappiness + +su - admin -c 'ulimit -n' +cat /sys/block/$DISK/queue/scheduler + +if [ -d ${HOME}/tmpfs ] ; then + echo "tmpfs exist, do nothing." +else + ln -s /dev/shm ${HOME}/tmpfs + echo "create tmpfs ok" +fi diff --git a/distribution/bin/play.cmd b/distribution/bin/play.cmd new file mode 100644 index 0000000..f1737d5 --- /dev/null +++ b/distribution/bin/play.cmd @@ -0,0 +1,30 @@ +@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. + +START /B mqnamesrv > ns.log 2>&1 +IF %ERRORLEVEL% NEQ 0 ( + echo "Failed to start name server. Please check ns.log" + EXIT /B 1 +) + +START /B mqbroker -n localhost:9876 > bk.log 2>&1 + +IF %ERRORLEVEL% NEQ 0 ( + ECHO "Failed to start broker. Please check bk.log" + EXIT /B 1 +) + +echo "Start Name Server and Broker Successfully." \ No newline at end of file diff --git a/distribution/bin/play.sh b/distribution/bin/play.sh new file mode 100644 index 0000000..359d18d --- /dev/null +++ b/distribution/bin/play.sh @@ -0,0 +1,33 @@ +#!/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. + +# +# Name Server +# +nohup sh mqnamesrv > ns.log 2>&1 & + +# +# Service Addr +# +ADDR=`hostname -i`:9876 + +# +# Broker +# +nohup sh mqbroker -n ${ADDR} > bk.log 2>&1 & + +echo "Start Name Server and Broker Successfully, ${ADDR}" diff --git a/distribution/bin/runbroker.cmd b/distribution/bin/runbroker.cmd new file mode 100644 index 0000000..f9a7109 --- /dev/null +++ b/distribution/bin/runbroker.cmd @@ -0,0 +1,60 @@ +@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 "%JAVA_HOME%\bin\java.exe" echo Please set the JAVA_HOME variable in your environment, We need java(x64)! & EXIT /B 1 +set "JAVA=%JAVA_HOME%\bin\java.exe" + +setlocal + +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;%BASE_DIR%lib\*;"%CLASSPATH%" + +rem =========================================================================================== +rem JVM Configuration +rem =========================================================================================== +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 -XX:+IgnoreUnrecognizedVMOptions" + set "JAVA_OPT=%JAVA_OPT% %JAVA_OPT_EXT% -cp "%CLASSPATH%"" +) + +"%JAVA%" %JAVA_OPT% %* diff --git a/distribution/bin/runbroker.sh b/distribution/bin/runbroker.sh new file mode 100644 index 0000000..e701e6c --- /dev/null +++ b/distribution/bin/runbroker.sh @@ -0,0 +1,123 @@ +#!/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. + +#=========================================================================================== +# Java Environment Setting +#=========================================================================================== +error_exit () +{ + echo "ERROR: $1 !!" + 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)!" + +export JAVA_HOME +export JAVA="$JAVA_HOME/bin/java" +export BASE_DIR=$(dirname $0)/.. +export CLASSPATH=.:${BASE_DIR}/conf:${BASE_DIR}/lib/*:${CLASSPATH} + +#=========================================================================================== +# JVM Configuration +#=========================================================================================== +# 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 -XX:+IgnoreUnrecognizedVMOptions" +#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}" + +numactl --interleave=all pwd > /dev/null 2>&1 +if [ $? -eq 0 ] +then + if [ -z "$RMQ_NUMA_NODE" ] ; then + numactl --interleave=all $JAVA ${JAVA_OPT} $@ + else + numactl --cpunodebind=$RMQ_NUMA_NODE --membind=$RMQ_NUMA_NODE $JAVA ${JAVA_OPT} $@ + fi +else + "$JAVA" ${JAVA_OPT} $@ +fi diff --git a/distribution/bin/runserver.cmd b/distribution/bin/runserver.cmd new file mode 100644 index 0000000..103a5a6 --- /dev/null +++ b/distribution/bin/runserver.cmd @@ -0,0 +1,56 @@ +@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 "%JAVA_HOME%\bin\java.exe" echo Please set the JAVA_HOME variable in your environment, We need java(x64)! & EXIT /B 1 +set "JAVA=%JAVA_HOME%\bin\java.exe" + +setlocal + +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;%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 new file mode 100644 index 0000000..2a5184d --- /dev/null +++ b/distribution/bin/runserver.sh @@ -0,0 +1,108 @@ +#!/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. + +#=========================================================================================== +# Java Environment Setting +#=========================================================================================== +error_exit () +{ + echo "ERROR: $1 !!" + 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)!" + +export JAVA_HOME +export JAVA="$JAVA_HOME/bin/java" +export BASE_DIR=$(dirname $0)/.. +export CLASSPATH=.:${BASE_DIR}/conf:${BASE_DIR}/lib/*:${CLASSPATH} + +#=========================================================================================== +# JVM Configuration +#=========================================================================================== +# 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} -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} $@ diff --git a/distribution/bin/setcache.sh b/distribution/bin/setcache.sh new file mode 100644 index 0000000..27633f3 --- /dev/null +++ b/distribution/bin/setcache.sh @@ -0,0 +1,40 @@ +#!/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. + +export PATH=$PATH:/sbin + +# +# GB +# +function changeFreeCache() +{ + EXTRA=$1 + MIN=$2 + sysctl -w vm.extra_free_kbytes=${EXTRA}000000 + sysctl -w vm.min_free_kbytes=${MIN}000000 + sysctl -w vm.swappiness=0 +} + + +if [ $# -ne 2 ] +then + echo "Usage: $0 extra_free_kbytes(GB) min_free_kbytes(GB)" + echo "Example: $0 3 1" + exit +fi + +changeFreeCache $1 $2 diff --git a/distribution/bin/startfsrv.sh b/distribution/bin/startfsrv.sh new file mode 100644 index 0000000..f7ba188 --- /dev/null +++ b/distribution/bin/startfsrv.sh @@ -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 + +nohup sh ${ROCKETMQ_HOME}/bin/runserver.sh org.apache.rocketmq.filtersrv.FiltersrvStartup $@ & diff --git a/distribution/bin/tools.cmd b/distribution/bin/tools.cmd new file mode 100644 index 0000000..263700d --- /dev/null +++ b/distribution/bin/tools.cmd @@ -0,0 +1,34 @@ +@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 "%JAVA_HOME%\bin\java.exe" echo Please set the JAVA_HOME variable in your environment, We need java(x64)! & EXIT /B 1 + +set "JAVA=%JAVA_HOME%\bin\java.exe" + +setlocal +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;%BASE_DIR%lib\*;%CLASSPATH% + +rem =========================================================================================== +rem JVM Configuration +rem =========================================================================================== +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% %* diff --git a/distribution/bin/tools.sh b/distribution/bin/tools.sh new file mode 100644 index 0000000..9b1e1d8 --- /dev/null +++ b/distribution/bin/tools.sh @@ -0,0 +1,60 @@ +#!/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. + +#=========================================================================================== +# Java Environment Setting +#=========================================================================================== +error_exit () +{ + echo "ERROR: $1 !!" + 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)!" + +export JAVA_HOME +export JAVA="$JAVA_HOME/bin/java" +export BASE_DIR=$(dirname $0)/.. +export CLASSPATH=.:${BASE_DIR}/conf:${BASE_DIR}/lib/*:${CLASSPATH} + +#=========================================================================================== +# JVM Configuration +#=========================================================================================== +JAVA_OPT="${JAVA_OPT} -server -Xms1g -Xmx1g -Xmn256m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m" +JAVA_OPT="${JAVA_OPT} -cp ${CLASSPATH}" + +"$JAVA" ${JAVA_OPT} "$@" diff --git a/distribution/conf/2m-2s-async/broker-a-s.properties b/distribution/conf/2m-2s-async/broker-a-s.properties new file mode 100644 index 0000000..0e3388b --- /dev/null +++ b/distribution/conf/2m-2s-async/broker-a-s.properties @@ -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. +brokerClusterName=DefaultCluster +brokerName=broker-a +brokerId=1 +deleteWhen=04 +fileReservedTime=48 +brokerRole=SLAVE +flushDiskType=ASYNC_FLUSH diff --git a/distribution/conf/2m-2s-async/broker-a.properties b/distribution/conf/2m-2s-async/broker-a.properties new file mode 100644 index 0000000..b704b54 --- /dev/null +++ b/distribution/conf/2m-2s-async/broker-a.properties @@ -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. +brokerClusterName=DefaultCluster +brokerName=broker-a +brokerId=0 +deleteWhen=04 +fileReservedTime=48 +brokerRole=ASYNC_MASTER +flushDiskType=ASYNC_FLUSH diff --git a/distribution/conf/2m-2s-async/broker-b-s.properties b/distribution/conf/2m-2s-async/broker-b-s.properties new file mode 100644 index 0000000..6c6beab --- /dev/null +++ b/distribution/conf/2m-2s-async/broker-b-s.properties @@ -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. +brokerClusterName=DefaultCluster +brokerName=broker-b +brokerId=1 +deleteWhen=04 +fileReservedTime=48 +brokerRole=SLAVE +flushDiskType=ASYNC_FLUSH diff --git a/distribution/conf/2m-2s-async/broker-b.properties b/distribution/conf/2m-2s-async/broker-b.properties new file mode 100644 index 0000000..130671a --- /dev/null +++ b/distribution/conf/2m-2s-async/broker-b.properties @@ -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. +brokerClusterName=DefaultCluster +brokerName=broker-b +brokerId=0 +deleteWhen=04 +fileReservedTime=48 +brokerRole=ASYNC_MASTER +flushDiskType=ASYNC_FLUSH diff --git a/distribution/conf/2m-2s-sync/broker-a-s.properties b/distribution/conf/2m-2s-sync/broker-a-s.properties new file mode 100644 index 0000000..0e3388b --- /dev/null +++ b/distribution/conf/2m-2s-sync/broker-a-s.properties @@ -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. +brokerClusterName=DefaultCluster +brokerName=broker-a +brokerId=1 +deleteWhen=04 +fileReservedTime=48 +brokerRole=SLAVE +flushDiskType=ASYNC_FLUSH diff --git a/distribution/conf/2m-2s-sync/broker-a.properties b/distribution/conf/2m-2s-sync/broker-a.properties new file mode 100644 index 0000000..fba30fa --- /dev/null +++ b/distribution/conf/2m-2s-sync/broker-a.properties @@ -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. +brokerClusterName=DefaultCluster +brokerName=broker-a +brokerId=0 +deleteWhen=04 +fileReservedTime=48 +brokerRole=SYNC_MASTER +flushDiskType=ASYNC_FLUSH diff --git a/distribution/conf/2m-2s-sync/broker-b-s.properties b/distribution/conf/2m-2s-sync/broker-b-s.properties new file mode 100644 index 0000000..6c6beab --- /dev/null +++ b/distribution/conf/2m-2s-sync/broker-b-s.properties @@ -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. +brokerClusterName=DefaultCluster +brokerName=broker-b +brokerId=1 +deleteWhen=04 +fileReservedTime=48 +brokerRole=SLAVE +flushDiskType=ASYNC_FLUSH diff --git a/distribution/conf/2m-2s-sync/broker-b.properties b/distribution/conf/2m-2s-sync/broker-b.properties new file mode 100644 index 0000000..ab925a8 --- /dev/null +++ b/distribution/conf/2m-2s-sync/broker-b.properties @@ -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. +brokerClusterName=DefaultCluster +brokerName=broker-b +brokerId=0 +deleteWhen=04 +fileReservedTime=48 +brokerRole=SYNC_MASTER +flushDiskType=ASYNC_FLUSH diff --git a/distribution/conf/2m-noslave/broker-a.properties b/distribution/conf/2m-noslave/broker-a.properties new file mode 100644 index 0000000..b704b54 --- /dev/null +++ b/distribution/conf/2m-noslave/broker-a.properties @@ -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. +brokerClusterName=DefaultCluster +brokerName=broker-a +brokerId=0 +deleteWhen=04 +fileReservedTime=48 +brokerRole=ASYNC_MASTER +flushDiskType=ASYNC_FLUSH diff --git a/distribution/conf/2m-noslave/broker-b.properties b/distribution/conf/2m-noslave/broker-b.properties new file mode 100644 index 0000000..130671a --- /dev/null +++ b/distribution/conf/2m-noslave/broker-b.properties @@ -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. +brokerClusterName=DefaultCluster +brokerName=broker-b +brokerId=0 +deleteWhen=04 +fileReservedTime=48 +brokerRole=ASYNC_MASTER +flushDiskType=ASYNC_FLUSH diff --git a/distribution/conf/2m-noslave/broker-trace.properties b/distribution/conf/2m-noslave/broker-trace.properties new file mode 100644 index 0000000..9dd57a7 --- /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/broker.conf b/distribution/conf/broker.conf new file mode 100644 index 0000000..0c0b28b --- /dev/null +++ b/distribution/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/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 0000000..8da6011 --- /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 0000000..c654bf8 --- /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 0000000..6e8f896 --- /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 0000000..cf9f803 --- /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 0000000..f50165d --- /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 0000000..0870bfd --- /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 0000000..fd700c1 --- /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 0000000..d574137 --- /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 0000000..f6dec22 --- /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 0000000..aa45fa5 --- /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 0000000..ee2f5df --- /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 0000000..9461321 --- /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 0000000..aee7e99 --- /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 0000000..700908f --- /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 0000000..c397689 --- /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 0000000..33bab6b --- /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 0000000..a7d81b0 --- /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 0000000..5351e49 --- /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 0000000..6aaf8f9 --- /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 0000000..c863d89 --- /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/rmq-proxy.json b/distribution/conf/rmq-proxy.json new file mode 100644 index 0000000..8e92bb1 --- /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 0000000..9a37259 --- /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 new file mode 100644 index 0000000..a285f69 --- /dev/null +++ b/distribution/pom.xml @@ -0,0 +1,127 @@ + + + + 4.0.0 + + org.apache.rocketmq + rocketmq-all + 5.3.4-SNAPSHOT + + 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-proxy + + + org.apache.rocketmq + rocketmq-client + + + org.apache.rocketmq + rocketmq-tools + + + org.apache.rocketmq + rocketmq-example + + + + + + + maven-assembly-plugin + + + release-all + + single + + package + + + release.xml + + false + + + + + + rocketmq-${project.version} + + + + + release-client + + + org.apache.rocketmq + rocketmq-client + + + + + + + maven-assembly-plugin + + + release-client + + single + + package + + + release-client.xml + + false + + + + + + rocketmq-client-${project.version} + + + + + \ No newline at end of file diff --git a/distribution/release-client.xml b/distribution/release-client.xml new file mode 100644 index 0000000..f787c33 --- /dev/null +++ b/distribution/release-client.xml @@ -0,0 +1,65 @@ + + + + client + true + + dir + tar.gz + zip + + + + + ../ + + README.md + + + + + + + LICENSE-BIN + LICENSE + + + NOTICE-BIN + NOTICE + + + + + + true + + org.apache.rocketmq:rocketmq-client + org.apache.rocketmq:rocketmq-openmessaging + + + ./ + false + + + ./ + + + + + + diff --git a/distribution/release.xml b/distribution/release.xml new file mode 100644 index 0000000..b771042 --- /dev/null +++ b/distribution/release.xml @@ -0,0 +1,112 @@ + + + + all + true + + dir + tar.gz + zip + + + + ../ + + README.md + + + + + + conf/** + benchmark/* + + + + + + bin/** + + 0755 + + + + + + LICENSE-BIN + LICENSE + + + 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-example + org.apache.rocketmq:rocketmq-openmessaging + org.apache.rocketmq:rocketmq-controller + + + lib/ + false + + + 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 0000000..a4de988 --- /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 0000000..b85d365 --- /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 0000000..9ff03e5 --- /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 0000000..92cb801 --- /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 +``` +3. `Idea-Edit Configurations`中添加运行参数 `-c /Users/xxx/rocketmq/conf/broker.conf` 以及环境变量 `ROCKETMQ_HOME=` +![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 0000000..c13f328 --- /dev/null +++ b/docs/cn/Deployment.md @@ -0,0 +1,170 @@ +# 部署架构和设置步骤 + +## 集群的设置 + +### 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.168.1.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 0000000..4edac9a --- /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 0000000..6ebb5a9 --- /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 0000000..ee97529 --- /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 0000000..31df40f --- /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 0000000..85a3db5 --- /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 0000000..f0a2b6a --- /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 0000000..b588079 --- /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 0000000..bbeb941 --- /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 0000000..acfbd2f --- /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 0000000..77c1bd7 --- /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(final String 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. 事务消息的生产者 GroupName 不能与其他类型消息的生产者 GroupName 共享。与其他类型的消息不同,事务消息允许反向查询、MQ服务器能通过它们的生产者 GroupName 查询到生产者。 + +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 0000000..b64adc6 --- /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信息上报,过于“重”,而且注册间隔过于长,因此需要一个轻量级的心跳机制,RocketMQ 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的信息,RocketMQ 5.0会新增GetBrokerMemberGroup请求来完成该工作。 + +Slave Broker发现自己是该组中最小的brokerId,将会开启代理模式,而一旦Master Broker重新上线,Slave Broker同样会通过Nameserver反向通知或自身定时任务同步同组broker的信息感知到,并自动结束代理模式。 + +### 二级消息逃逸 + +代理模式开启后,brokerId最小的Slave会承担起二级消息的扫描和重新投递功能。 + +二级消息一般分为两个阶段,发送或者消费时会发送到一个特殊topic中,后台会有线程会扫描,最终的满足要求的消息会被重新投递到Commitlog中。我们可以让brokerId最小的Slave进行扫描,但如果扫描之后的消息重新投递到本Commitlog,那将会破坏Slave不可写的语义,造成Commitlog分叉。因此RocketMQ 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_设计.md b/docs/cn/acl/RocketMQ_Multiple_ACL_Files_设计.md new file mode 100644 index 0000000..d457b67 --- /dev/null +++ b/docs/cn/acl/RocketMQ_Multiple_ACL_Files_设计.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 0000000..463a28d --- /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 0000000..87e93d1 --- /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 0000000..0f621b0 --- /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 | 通信层异步回调线程数 | +| pollNameServerInterval | 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 0000000..66857e2 --- /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 0000000..36ad323 --- /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 0000000..3d67e93 --- /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 0000000..78816e0 --- /dev/null +++ b/docs/cn/controller/deploy.md @@ -0,0 +1,183 @@ +# 部署和升级指南 + +## 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代码。 + +> 5.2.0 Controller 开始支持 jRaft 内核启动,不支持 DLedger 内核到 jRaft 内核原地升级 + +``` +controllerType = jRaft +jRaftGroupId = jRaft-Controller +jRaftServerId = localhost:9880 +jRaftInitConf = localhost:9880,localhost:9881,localhost:9882 +jRaftControllerRPCAddr = localhost:9770,localhost:9771,localhost:9772 +jRaftSnapshotIntervalSecs = 3600 +``` + +jRaft 版本相关参数 +- controllerType:controllerType=jRaft的时候内核启动使用jRaft,默认为DLedger。 +- jRaftGroupId:jRaft Group的名字,同一个jRaft Group保持一致即可。 +- jRaftServerId:标志自己节点的ServerId,必须出现在 jRaftInitConf 中。 +- jRaftInitConf:jRaft Group 内部通信各节点的地址信息,用逗号分隔,是 jRaft 内部通信来做选举和复制所用的地址。 +- jRaftControllerRPCAddr:Controller 外部通信的各节点的地址信息,用逗号分隔,比如 Controller 与 Broker 通信会使用该地址。 +- jRaftSnapshotIntervalSecs:Raft Snapshot 持久化时间间隔。 + +参数设置完成后,指定配置文件启动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 0000000..13eba77 --- /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) + slaveBrokerId(8byte)` + +- Current state 代表当前的 HAConnectionState,也即 HANDSHAKE。 + +- Two flags 是两个状态标志位,其中,isSyncFromLastFile 代表是否要从 Master 的最后一个文件开始复制,isAsyncLearner 代表该 Slave 是否是异步复制,并以 Learner 的形式接入 Master。 + +- slaveBrokerId 代表了该 Slave 的 brokerId,用于后续加入 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 0000000..1d7a289 --- /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 0000000..5cc7d6d --- /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 0000000..00b4de3 --- /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 0000000..2d04590 --- /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 0000000..8311395 --- /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 0000000..ab67683 --- /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 0000000..6fbedcf 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 0000000..65edd99 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 0000000..3afd088 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 0000000..ae00da6 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 0000000..fea8256 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 0000000..a823394 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 0000000..0379c23 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 0000000..308b936 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 0000000..01b33ca 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 0000000..a909a70 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 0000000..0689bd0 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 0000000..cee8ddf 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 0000000..32425d2 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 0000000..a454ead 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 0000000..2000157 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 0000000..d6df0aa 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 0000000..6f48659 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 0000000..d7ffed6 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 0000000..67dd768 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 0000000..696a4c3 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 0000000..016feda 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 0000000..addb571 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 0000000..b2ab8d3 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 0000000..b5d755a 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 0000000..8c719e1 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 0000000..df75da5 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 0000000..e3dc741 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 0000000..bf95dbc 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 0000000..32ba455 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 0000000..1610ae0 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 0000000..a0796ed 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 0000000..bc89818 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 0000000..a52d31e 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 0000000..8b675d8 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 0000000..b0faa86 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 0000000..ab4a1fb 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 0000000..4af0416 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 0000000..a04c260 --- /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 message 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 0000000..9f04ce1 --- /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 0000000..5ecc138 --- /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 0000000..ffd2bb8 --- /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_设计.md b/docs/cn/statictopic/RocketMQ_Static_Topic_Logic_Queue_设计.md new file mode 100644 index 0000000..784a83f --- /dev/null +++ b/docs/cn/statictopic/RocketMQ_Static_Topic_Logic_Queue_设计.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 0000000..886e33e --- /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 0000000..208ae44 --- /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 0000000..03f2384 --- /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 0000000..af83ffc --- /dev/null +++ b/docs/en/Configuration_Client.md @@ -0,0 +1,107 @@ +## Client Configuration + +Relative to RocketMQ's Broker cluster, producers and consumers are clients. This section describes the common behavior configuration of producers and consumers, including updates for **RocketMQ 5.x**. + +### 1 Client Addressing Mode + +`RocketMQ` allows clients to locate the `Name Server`, which then helps them find the `Broker`. Below are various configurations, prioritized from highest to lowest. Higher priority configurations override lower ones. + +- Specified `Name Server` address in the code (multiple addresses 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 Java setup parameters: + +```text +-Drocketmq.namesrv.addr=192.168.0.1:9876;192.168.0.2:9876 +``` + +- Specified `Name Server` address in environment variables: + +```text +export NAMESRV_ADDR=192.168.0.1:9876;192.168.0.2:9876 +``` + +- HTTP static server addressing (default): + +Clients retrieve `Name Server` addresses from a static HTTP server: +```text +http://jmenv.tbsite.net:8080/rocketmq/nsaddr +``` + +- **New in RocketMQ 5.x:** + - Improved service discovery mechanism, allowing dynamic Name Server registration. + - Introduces `VIP_CHANNEL_ENABLED` for better failover: + + ```java + producer.setVipChannelEnabled(false); + consumer.setVipChannelEnabled(false); + ``` + +### 2 Client Configuration + +`DefaultMQProducer`, `TransactionMQProducer`, `DefaultMQPushConsumer`, and `DefaultMQPullConsumer` all extend the `ClientConfig` class, which provides common client configurations. + +#### 2.1 Client Common Configuration + +| Parameter Name | Default Value | Description | +|-------------------------------|---------------|--------------| +| namesrvAddr | | Name Server address list (semicolon-separated) | +| clientIP | Local IP | Client's local IP address (useful if automatic detection fails) | +| instanceName | DEFAULT | Unique name for the client instance | +| clientCallbackExecutorThreads | 4 | Number of communication layer asynchronous callback threads | +| pollNameServerInterval | 30000 | Interval (ms) to poll Name Server | +| heartbeatBrokerInterval | 30000 | Interval (ms) for sending heartbeats to Broker | +| persistConsumerOffsetInterval | 5000 | Interval (ms) for persisting consumer offsets | +| **autoUpdateNameServer** (5.x) | true | Automatically update Name Server addresses from registry | +| **instanceId** (5.x) | | Unique identifier for each client instance | + +#### 2.2 Producer Configuration + +| Parameter Name | Default Value | Description | +|--------------------------------|----------------------|--------------| +| producerGroup | DEFAULT_PRODUCER | Producer group name | +| sendMsgTimeout | 3000 | Timeout (ms) for sending messages | +| retryTimesWhenSendFailed | 2 | Max retries for failed messages | +| **enableBatchSend** (5.x) | true | Enables batch message sending | +| **enableBackPressure** (5.x) | true | Prevents overload in high-traffic scenarios | + +#### 2.3 PushConsumer Configuration + +| Parameter Name | Default Value | Description | +|--------------------------------------|------------------------------------|-------------| +| consumerGroup | DEFAULT_CONSUMER | Consumer group name | +| messageModel | CLUSTERING | Message consumption mode (CLUSTERING or BROADCAST) | +| consumeFromWhere | CONSUME_FROM_LAST_OFFSET | Default consumption position | +| consumeThreadMin | 20 | Min consumption thread count | +| consumeThreadMax | 20 | Max consumption thread count | +| **Rebalance Strategies (5.x)** | AllocateMessageQueueAveragelyByCircle | New rebalance strategy for better distribution | + +#### 2.4 PullConsumer Configuration + +| Parameter Name | Default Value | Description | +|------------------------------------|------------------------------|-------------| +| consumerGroup | DEFAULT_CONSUMER | Consumer group name | +| brokerSuspendMaxTimeMillis | 20000 | Max suspension time (ms) for long polling | +| consumerPullTimeoutMillis | 10000 | Timeout (ms) for pull requests | +| **messageGroup** (5.x) | | Enables orderly consumption based on groups | + +#### 2.5 Message Data Structure + +| Field Name | Default Value | Description | +|-------------------|---------------|-------------| +| Topic | null | Required: Name of the message topic | +| Body | null | Required: Message content | +| Tags | null | Optional: Tag for filtering | +| Keys | null | Optional: Business keys (e.g., order IDs) | +| Flag | 0 | Optional: Custom flag | +| DelayTimeLevel | 0 | Optional: Message delay level | +| WaitStoreMsgOK | TRUE | Optional: Acknowledgment before storing | +| **maxReconsumeTimes** (5.x) | | Max retries before moving to dead-letter queue | +| **messageGroup** (5.x) | | Group-based message ordering | + +--- + diff --git a/docs/en/Configuration_System.md b/docs/en/Configuration_System.md new file mode 100644 index 0000000..95589d9 --- /dev/null +++ b/docs/en/Configuration_System.md @@ -0,0 +1,57 @@ +# The System Configuration (RocketMQ 5.x) + +This section focuses on the configuration of the system (JVM/OS) for **RocketMQ 5.x**. + +## **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 to potentially reduce JVM pauses: + + -XX:-UseBiasedLocking + +As for garbage collection, the G1 collector with JDK 1.8 is recommended: + + -XX:+UseG1GC -XX:G1HeapRegionSize=16m + -XX:G1ReservePercent=25 + -XX:InitiatingHeapOccupancyPercent=30 + +These GC options may seem a bit aggressive, but they have been proven to provide good performance in our production environment. + +Do not set a too small value for -XX:MaxGCPauseMillis, as it may cause the JVM to use a small young generation, leading to frequent minor GCs. Using a rolling GC log file is recommended: + + -XX:+UseGCLogFileRotation + -XX:NumberOfGCLogFiles=5 + -XX:GCLogFileSize=30m + +If writing GC logs to disk increases broker latency, consider redirecting the GC log file to a memory file system: + + -Xloggc:/dev/shm/mq_gc_%p.log123 + +## **2 Linux Kernel Parameters** ## + +There is an `os.sh` script in the `bin` folder that lists several kernel parameters that can be used for production with minor modifications. Below parameters require attention. For more details, refer to the 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. (Kernel version-specific) + +- **vm.min_free_kbytes**: If set lower than 1024KB, the system may become subtly broken and prone to deadlocks under high loads. + +- **vm.max_map_count**: Limits the maximum number of memory map areas a process may have. RocketMQ uses `mmap` to load `CommitLog` and `ConsumeQueue`, so setting a higher value is recommended. + +- **vm.swappiness**: Defines how aggressively the kernel swaps memory pages. Higher values increase aggressiveness, while lower values decrease the amount of swap. A value of **10** is recommended to avoid swap latency. + +- **File descriptor limits**: RocketMQ requires open file descriptors for files (`CommitLog` and `ConsumeQueue`) and network connections. It is recommended to set this to **655350**. + +- **Disk scheduler**: The **deadline I/O scheduler** is recommended for RocketMQ as it 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 0000000..445d186 --- /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 0000000..0dee903 --- /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 environment variable `ROCKETMQ_HOME=` 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 +``` +3. Add the runtime parameter `-c /Users/xxx/rocketmq/conf/broker.conf` and the environment variable `ROCKETMQ_HOME=` 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 environment variable `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 0000000..8f5c8f1 --- /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.168.1.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 0000000..7ccb94f --- /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 0000000..86c47b1 --- /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 0000000..887a073 --- /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 0000000..46e001a --- /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 0000000..747c98e --- /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 0000000..930697b --- /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 0000000..e62f463 --- /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 0000000..ed55286 --- /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 0000000..c3fb5a6 --- /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 0000000..a136d25 --- /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 0000000..768692d --- /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 0000000..a79fd10 --- /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 0000000..e9cde37 --- /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 0000000..0ce4924 --- /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 0000000..e217073 --- /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 0000000..dac53ec --- /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 0000000..c51b965 --- /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 0000000..cf3ea58 --- /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 0000000..8944be1 --- /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 0000000..c7f0d57 --- /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 0000000..74c2cde --- /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 0000000..bd8c565 --- /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 0000000..2a096c3 --- /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 0000000..48011af --- /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 0000000..ee4adab --- /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 0000000..0651ea8 --- /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 0000000..863a622 --- /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 0000000..582f9c4 --- /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 0000000..e32422b --- /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 0000000..8484951 --- /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 0000000..af4958a --- /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) + slaveBrokerId(8byte)` + +- `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. + +- `slaveBrokerId` represent the brokerId 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 0000000..cd07657 --- /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 0000000..e799721 --- /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 0000000..c919862 --- /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 0000000..06cf333 --- /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 0000000..dfc7894 --- /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 0000000..fea8256 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 0000000..a823394 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 0000000..0379c23 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 0000000..308b936 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 0000000..01b33ca 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 0000000..a909a70 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 0000000..0689bd0 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 0000000..cee8ddf 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 0000000..32425d2 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 0000000..a454ead 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 0000000..2000157 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 0000000..d6df0aa 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 0000000..6f48659 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 0000000..d7ffed6 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 0000000..67dd768 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 0000000..696a4c3 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 0000000..addb571 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 0000000..b2ab8d3 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 0000000..b5d755a 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 0000000..b0faa86 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 0000000..f5ca945 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 0000000..1b4eb5e 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 0000000..12e6354 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 0000000..8c719e1 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 0000000..1610ae0 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 0000000..64c4b2b --- /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 0000000..a6b707b --- /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 0000000..3469648 --- /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 new file mode 100644 index 0000000..9014beb --- /dev/null +++ b/example/pom.xml @@ -0,0 +1,80 @@ + + + + + rocketmq-all + org.apache.rocketmq + 5.3.4-SNAPSHOT + + + 4.0.0 + jar + rocketmq-example + rocketmq-example ${project.version} + + + ${basedir}/.. + + + + + ${project.groupId} + rocketmq-client + + + ${project.groupId} + rocketmq-tools + + + ${project.groupId} + rocketmq-remoting + + + ${project.groupId} + rocketmq-srvutil + + + ${project.groupId} + rocketmq-openmessaging + + + ${project.groupId} + rocketmq-auth + + + org.javassist + javassist + + + io.jaegertracing + jaeger-core + + + 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 new file mode 100644 index 0000000..cf82c2a --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/batch/SimpleBatchProducer.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.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(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 + List messages = new ArrayList<>(); + 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))); + + 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 new file mode 100644 index 0000000..d33a5a5 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/batch/SplitBatchProducer.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.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(PRODUCER_GROUP); + // Uncomment the following line while debugging, namesrvAddr should be set to your local address +// producer.setNamesrvAddr(DEFAULT_NAMESRVADDR); + producer.start(); + + //large batch + 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(); + SendResult sendResult = producer.send(listItem); + System.out.printf("%s", sendResult); + } + } + +} + +class ListSplitter implements Iterator> { + private static final int SIZE_LIMIT = 1000 * 1000; + 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 nextIndex = currIndex; + int totalSize = 0; + for (; nextIndex < messages.size(); nextIndex++) { + Message message = messages.get(nextIndex); + 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(); + } + //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) { + //if the next sublist has no element, add this one and then break, otherwise just break + nextIndex++; + } + break; + } + if (tmpSize + totalSize > SIZE_LIMIT) { + break; + } else { + totalSize += tmpSize; + } + + } + List subList = messages.subList(currIndex, nextIndex); + currIndex = nextIndex; + return subList; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Not allowed to remove"); + } +} 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 0000000..b3d6fb4 --- /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 0000000..21a4b3b --- /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.setCompressType(CompressionType.of(compressType)); + producer.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 new file mode 100644 index 0000000..57270fc --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/benchmark/Consumer.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.example.benchmark; + +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; +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.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 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 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(isSuffixEnable)) { + group = groupPrefix + "_" + (System.currentTimeMillis() % 100); + } + + 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(); + + ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1, + new BasicThreadFactory.Builder().namingPattern("BenchmarkTimerThread-%d").daemon(true).build()); + + final LinkedList snapshotList = new LinkedList<>(); + + executorService.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + snapshotList.addLast(statsBenchmarkConsumer.createSnapshot()); + if (snapshotList.size() > 10) { + snapshotList.removeFirst(); + } + } + }, 1000, 1000, TimeUnit.MILLISECONDS); + + executorService.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 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("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); + } + } + + @Override + public void run() { + try { + this.printStats(); + } catch (Exception e) { + e.printStackTrace(); + } + } + }, reportInterval, reportInterval, TimeUnit.MILLISECONDS); + + 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, "*"); + } else { + if (ExpressionType.TAG.equals(filterType)) { + String expr = MixAll.file2String(expression); + System.out.printf("Expression: %s%n", expr); + consumer.subscribe(topic, MessageSelector.byTag(expr)); + } else if (ExpressionType.SQL92.equals(filterType)) { + String expr = MixAll.file2String(expression); + System.out.printf("Expression: %s%n", expr); + consumer.subscribe(topic, MessageSelector.bySql(expr)); + } else { + throw new IllegalArgumentException("Not support filter type! " + filterType); + } + } + + consumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, + ConsumeConcurrentlyContext context) { + MessageExt msg = msgs.get(0); + long now = System.currentTimeMillis(); + + statsBenchmarkConsumer.getReceiveMessageTotalCount().increment(); + + long born2ConsumerRT = now - msg.getBornTimestamp(); + statsBenchmarkConsumer.getBorn2ConsumerTotalRT().add(born2ConsumerRT); + + long store2ConsumerRT = now - msg.getStoreTimestamp(); + statsBenchmarkConsumer.getStore2ConsumerTotalRT().add(store2ConsumerRT); + + compareAndSetMax(statsBenchmarkConsumer.getBorn2ConsumerMaxRT(), born2ConsumerRT); + + compareAndSetMax(statsBenchmarkConsumer.getStore2ConsumerMaxRT(), store2ConsumerRT); + + if (ThreadLocalRandom.current().nextDouble() < failRate) { + statsBenchmarkConsumer.getFailCount().increment(); + return ConsumeConcurrentlyStatus.RECONSUME_LATER; + } else { + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + } + }); + + consumer.start(); + + System.out.printf("Consumer Started.%n"); + } + + public static Options buildCommandlineOptions(final Options options) { + Option opt = new Option("t", "topic", true, "Topic name, Default: BenchmarkTest"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("w", "threadCount", true, "Thread count, Default: 20"); + opt.setRequired(false); + options.addOption(opt); + + 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); + + opt = new Option("f", "filterType", true, "TAG, SQL92"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("e", "expression", true, "filter expression content file path.ie: ./test/expr"); + 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; + } + + public static void compareAndSetMax(final AtomicLong target, final long value) { + long prev = target.get(); + while (value > prev) { + boolean updated = target.compareAndSet(prev, value); + if (updated) + break; + + prev = target.get(); + } + } +} + +class StatsBenchmarkConsumer { + private final LongAdder receiveMessageTotalCount = new LongAdder(); + + private final LongAdder born2ConsumerTotalRT = new LongAdder(); + + 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.longValue(), + this.born2ConsumerTotalRT.longValue(), + this.store2ConsumerTotalRT.longValue(), + this.failCount.longValue() + }; + + return snap; + } + + public LongAdder getReceiveMessageTotalCount() { + return receiveMessageTotalCount; + } + + public LongAdder getBorn2ConsumerTotalRT() { + return born2ConsumerTotalRT; + } + + public LongAdder getStore2ConsumerTotalRT() { + return store2ConsumerTotalRT; + } + + public AtomicLong getBorn2ConsumerMaxRT() { + return born2ConsumerMaxRT; + } + + 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 new file mode 100644 index 0000000..a945283 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/benchmark/Producer.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.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.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.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.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 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 { + + 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 DefaultParser()); + if (null == commandLine) { + System.exit(-1); + } + + final String topic = commandLine.hasOption('t') ? commandLine.getOptionValue('t').trim() : "BenchmarkTest"; + 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; + 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(); + + ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1, + new BasicThreadFactory.Builder().namingPattern("BenchmarkTimerThread-%d").daemon(true).build()); + + final LinkedList snapshotList = new LinkedList<>(); + + final long[] msgNums = new long[threadCount]; + + 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()); + if (snapshotList.size() > 10) { + snapshotList.removeFirst(); + } + } + }, 1000, 1000, TimeUnit.MILLISECONDS); + + executorService.scheduleAtFixedRate(new TimerTask() { + private void printStats() { + if (snapshotList.size() >= 10) { + doPrintStats(snapshotList, statsBenchmark, false); + } + } + + @Override + public void run() { + try { + this.printStats(); + } catch (Exception e) { + e.printStackTrace(); + } + } + }, reportInterval, reportInterval, TimeUnit.MILLISECONDS); + + 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')) { + String ns = commandLine.getOptionValue('n'); + producer.setNamesrvAddr(ns); + } + + 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.setCompressType(CompressionType.of(compressType)); + producer.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 = 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(); + } + int i = 0; + int startValue = (new Random(System.currentTimeMillis())).nextInt(100); + int size = 0; + while (true) { + String prop1 = "prop" + i, prop1V = "hello" + startValue; + String prop2 = "prop" + (i + 1), prop2V = String.valueOf(startValue); + msg.putUserProperty(prop1, prop1V); + msg.putUserProperty(prop2, prop2V); + size += prop1.length() + prop2.length() + prop1V.length() + prop2V.length(); + if (size > propertySize) { + break; + } + i += 2; + startValue += 2; + } + } + 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().increment(); + log.error("[BENCHMARK_PRODUCER] Send Exception", e); + + try { + Thread.sleep(3000); + } catch (InterruptedException ignored) { + } + } catch (InterruptedException e) { + statsBenchmark.getSendRequestFailedCount().increment(); + try { + Thread.sleep(3000); + } catch (InterruptedException e1) { + } + } catch (MQClientException e) { + statsBenchmark.getSendRequestFailedCount().increment(); + log.error("[BENCHMARK_PRODUCER] Send Exception", e); + } catch (MQBrokerException e) { + 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) { + 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("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: 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 String topic) { + return new Message(topic, msgBody); + } + + private static void doPrintStats(final LinkedList snapshotList, final StatsBenchmarkProducer statsBenchmark, boolean done) { + 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]); + + 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 LongAdder sendRequestSuccessCount = new LongAdder(); + + private final LongAdder sendRequestFailedCount = new LongAdder(); + + private final LongAdder receiveResponseSuccessCount = new LongAdder(); + + private final LongAdder receiveResponseFailedCount = new LongAdder(); + + private final LongAdder sendMessageSuccessTimeTotal = new LongAdder(); + + private final AtomicLong sendMessageMaxRT = new AtomicLong(0L); + + public Long[] createSnapshot() { + Long[] snap = new Long[] { + System.currentTimeMillis(), + this.sendRequestSuccessCount.longValue(), + this.sendRequestFailedCount.longValue(), + this.receiveResponseSuccessCount.longValue(), + this.receiveResponseFailedCount.longValue(), + this.sendMessageSuccessTimeTotal.longValue(), + }; + + return snap; + } + + public LongAdder getSendRequestSuccessCount() { + return sendRequestSuccessCount; + } + + public LongAdder getSendRequestFailedCount() { + return sendRequestFailedCount; + } + + public LongAdder getReceiveResponseSuccessCount() { + return receiveResponseSuccessCount; + } + + public LongAdder getReceiveResponseFailedCount() { + return receiveResponseFailedCount; + } + + public LongAdder getSendMessageSuccessTimeTotal() { + return sendMessageSuccessTimeTotal; + } + + public AtomicLong getSendMessageMaxRT() { + return sendMessageMaxRT; + } +} 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 new file mode 100644 index 0000000..7b6350e --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/benchmark/TransactionProducer.java @@ -0,0 +1,496 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.LocalTransactionState; +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.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.RPCHook; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.SerializeType; +import org.apache.rocketmq.srvutil.ServerUtil; + +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 class TransactionProducer { + private static final long START_TIME = System.currentTimeMillis(); + private static final LongAdder MSG_COUNT = new LongAdder(); + + //broker max check times should less than this value + static final int MAX_CHECK_RESULT_IN_MSG = 20; + + 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(); + + ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1, + new BasicThreadFactory.Builder().namingPattern("BenchmarkTimerThread-%d").daemon(true).build()); + + final LinkedList snapshotList = new LinkedList<>(); + + executorService.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + snapshotList.addLast(statsBenchmark.createSnapshot()); + while (snapshotList.size() > 10) { + snapshotList.removeFirst(); + } + } + }, 1000, 1000, TimeUnit.MILLISECONDS); + + executorService.scheduleAtFixedRate(new TimerTask() { + private void printStats() { + if (snapshotList.size() >= 10) { + Snapshot begin = snapshotList.getFirst(); + Snapshot end = snapshotList.getLast(); + + 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( + "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); + } + } + + @Override + public void run() { + try { + this.printStats(); + } catch (Exception e) { + e.printStackTrace(); + } + } + }, config.reportInterval, config.reportInterval, TimeUnit.MILLISECONDS); + + 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.setTransactionListener(transactionCheckListener); + producer.setDefaultTopicQueueNums(1000); + if (commandLine.hasOption('n')) { + String ns = commandLine.getOptionValue('n'); + producer.setNamesrvAddr(ns); + } + producer.start(); + + 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 { + SendResult sendResult = + 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.getSendMessageTimeTotal().add(currentRT); + long prevMaxRT = statsBenchmark.getSendMessageMaxRT().get(); + while (currentRT > prevMaxRT) { + boolean updated = statsBenchmark.getSendMessageMaxRT() + .compareAndSet(prevMaxRT, currentRT); + if (updated) + break; + + prevMaxRT = statsBenchmark.getSendMessageMaxRT().get(); + } + if (success) { + statsBenchmark.getSendRequestSuccessCount().increment(); + } else { + statsBenchmark.getSendRequestFailedCount().increment(); + } + if (config.sendInterval > 0) { + try { + Thread.sleep(config.sendInterval); + } catch (InterruptedException e) { + } + } + } + } + } + }); + } + } + + 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()); + } + + // 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()); + } + } + + 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 TransactionListenerImpl implements TransactionListener { + private StatsBenchmarkTProducer statBenchmark; + private TxSendConfig sendConfig; + private final LRUMap cache = new LRUMap<>(200000); + + private class MsgMeta { + long batchId; + long msgId; + LocalTransactionState sendResult; + List checkResult; + } + + public TransactionListenerImpl(StatsBenchmarkTProducer statsBenchmark, TxSendConfig sendConfig) { + this.statBenchmark = statsBenchmark; + this.sendConfig = sendConfig; + } + + @Override + public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { + return parseFromMsg(msg).sendResult; + } + + 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 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; + } + + 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 Snapshot { + long endTime; + + long sendRequestSuccessCount; + + long sendRequestFailedCount; + + long sendMessageTimeTotal; + + long sendMessageMaxRT; + + long checkCount; + + long unexpectedCheckCount; + + 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 LongAdder checkCount = new LongAdder(); + + private final LongAdder unexpectedCheckCount = new LongAdder(); + + 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 LongAdder getSendRequestSuccessCount() { + return sendRequestSuccessCount; + } + + public LongAdder getSendRequestFailedCount() { + return sendRequestFailedCount; + } + + public LongAdder getSendMessageTimeTotal() { + return sendMessageTimeTotal; + } + + public AtomicLong getSendMessageMaxRT() { + return sendMessageMaxRT; + } + + public LongAdder getCheckCount() { + return checkCount; + } + + public LongAdder getUnexpectedCheckCount() { + return unexpectedCheckCount; + } + + 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 0000000..f2ab6b5 --- /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 0000000..3e92ff1 --- /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 new file mode 100644 index 0000000..e991dfe --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/broadcast/PushConsumer.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.broadcast; + +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; +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(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(TOPIC, SUB_EXPRESSION); + + 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("Broadcast 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 0000000..8dd6d20 --- /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 0000000..0018270 --- /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/TagFilterConsumer.java b/example/src/main/java/org/apache/rocketmq/example/filter/TagFilterConsumer.java new file mode 100644 index 0000000..74fc4a9 --- /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 0000000..b0a9e2d --- /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/lmq/LMQProducer.java b/example/src/main/java/org/apache/rocketmq/example/lmq/LMQProducer.java new file mode 100644 index 0000000..da6ad92 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/lmq/LMQProducer.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.lmq; + +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.MixAll; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.remoting.common.RemotingHelper; + +public class LMQProducer { + public static final String PRODUCER_GROUP = "ProducerGroupName"; + + public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876"; + + public static final String TOPIC = "TopicLMQParent"; + + public static final String TAG = "TagA"; + + public static final String LMQ_TOPIC_1 = MixAll.LMQ_PREFIX + "123"; + + public static final String LMQ_TOPIC_2 = MixAll.LMQ_PREFIX + "456"; + + public static void main(String[] args) throws MQClientException, InterruptedException { + 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(); + for (int i = 0; i < 128; i++) { + try { + Message msg = new Message(TOPIC, TAG, ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); + msg.setKeys("Key" + i); + msg.putUserProperty(MessageConst.PROPERTY_INNER_MULTI_DISPATCH /* "INNER_MULTI_DISPATCH" */, + String.join(MixAll.LMQ_DISPATCH_SEPARATOR, LMQ_TOPIC_1, LMQ_TOPIC_2) /* "%LMQ%123,%LMQ%456" */); + 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/lmq/LMQPullConsumer.java b/example/src/main/java/org/apache/rocketmq/example/lmq/LMQPullConsumer.java new file mode 100644 index 0000000..931dd96 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/lmq/LMQPullConsumer.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.example.lmq; + +import java.util.Arrays; +import java.util.HashSet; +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.client.exception.MQClientException; +import org.apache.rocketmq.common.MixAll; +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 LMQPullConsumer { + public static final String BROKER_NAME = "broker-a"; + + public static final String CONSUMER_GROUP = "CID_LMQ_PULL_1"; + + public static final String TOPIC = "TopicLMQParent"; + + public static final String LMQ_TOPIC = MixAll.LMQ_PREFIX + "123"; + + public static final String NAMESRV_ADDR = "127.0.0.1:9876"; + + public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException { + + DefaultMQPullConsumer consumer = new DefaultMQPullConsumer(CONSUMER_GROUP); + consumer.setNamesrvAddr(NAMESRV_ADDR); + consumer.setRegisterTopics(new HashSet<>(Arrays.asList(TOPIC))); + consumer.start(); + + // use parent topic to fill up broker addr table + consumer.getDefaultMQPullConsumerImpl().getRebalanceImpl().getmQClientFactory() + .updateTopicRouteInfoFromNameServer(TOPIC); + + final MessageQueue lmq = new MessageQueue(LMQ_TOPIC, BROKER_NAME, (int) MixAll.LMQ_QUEUE_ID); + long offset = consumer.minOffset(lmq); + + consumer.pullBlockIfNotFound(lmq, "*", offset, 32, new PullCallback() { + @Override + public void onSuccess(PullResult pullResult) { + List list = pullResult.getMsgFoundList(); + if (list == null || list.isEmpty()) { + return; + } + + for (MessageExt msg : list) { + System.out.printf("%s Pull New Messages: %s %n", Thread.currentThread().getName(), msg); + } + } + + @Override + public void onException(Throwable e) { + + } + }); + } +} diff --git a/example/src/main/java/org/apache/rocketmq/example/lmq/LMQPushConsumer.java b/example/src/main/java/org/apache/rocketmq/example/lmq/LMQPushConsumer.java new file mode 100644 index 0000000..f8926a0 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/lmq/LMQPushConsumer.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.example.lmq; + +import com.google.common.collect.Lists; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +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.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; + +public class LMQPushConsumer { + public static final String CLUSTER_NAME = "DefaultCluster"; + + public static final String BROKER_NAME = "broker-a"; + + public static final String TOPIC = "TopicLMQParent"; + + public static final String LMQ_TOPIC = MixAll.LMQ_PREFIX + "123"; + + public static final String CONSUMER_GROUP = "CID_LMQ_1"; + + public static final String NAMESRV_ADDR = "127.0.0.1:9876"; + + public static final HashMap BROKER_ADDR_MAP = new HashMap() { + { + put(MixAll.MASTER_ID, "127.0.0.1:10911"); + } + }; + + public static void main(String[] args) throws InterruptedException, MQClientException { + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP); + consumer.setNamesrvAddr(NAMESRV_ADDR); + consumer.subscribe(LMQ_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.start(); + + // use parent topic to fill up broker addr table + consumer.getDefaultMQPushConsumerImpl().getmQClientFactory().updateTopicRouteInfoFromNameServer(TOPIC); + + final TopicRouteData topicRouteData = new TopicRouteData(); + final BrokerData brokerData = new BrokerData(); + brokerData.setCluster(CLUSTER_NAME); + brokerData.setBrokerName(BROKER_NAME); + brokerData.setBrokerAddrs(BROKER_ADDR_MAP); + topicRouteData.setBrokerDatas(Lists.newArrayList(brokerData)); + // compensate LMQ topic route for MQClientInstance#findBrokerAddrByTopic + consumer.getDefaultMQPushConsumerImpl().getmQClientFactory().getTopicRouteTable().put(LMQ_TOPIC, topicRouteData); + // compensate for RebalanceImpl#topicSubscribeInfoTable + consumer.getDefaultMQPushConsumerImpl().updateTopicSubscribeInfo(LMQ_TOPIC, + new HashSet<>(Arrays.asList(new MessageQueue(LMQ_TOPIC, BROKER_NAME, (int) MixAll.LMQ_QUEUE_ID)))); + // re-balance immediately to start pulling messages + consumer.getDefaultMQPushConsumerImpl().getmQClientFactory().doRebalance(); + + System.out.printf("Consumer Started.%n"); + } +} diff --git a/example/src/main/java/org/apache/rocketmq/example/lmq/LMQPushPopConsumer.java b/example/src/main/java/org/apache/rocketmq/example/lmq/LMQPushPopConsumer.java new file mode 100644 index 0000000..517eb12 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/lmq/LMQPushPopConsumer.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.example.lmq; + +import com.google.common.collect.Lists; +import java.util.HashMap; +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.MixAll; +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.remoting.protocol.route.TopicRouteData; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; + +public class LMQPushPopConsumer { + public static final String CLUSTER_NAME = "DefaultCluster"; + + public static final String BROKER_NAME = "broker-a"; + + public static final String TOPIC = "TopicLMQParent"; + + public static final String LMQ_TOPIC = MixAll.LMQ_PREFIX + "456"; + + public static final String NAMESRV_ADDR = "127.0.0.1:9876"; + + public static final String CONSUMER_GROUP = "CID_LMQ_POP_1"; + + public static final HashMap BROKER_ADDR_MAP = new HashMap() { + { + put(MixAll.MASTER_ID, "127.0.0.1:10911"); + } + }; + + public static void main(String[] args) throws Exception { + switchPop(); + + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP); + consumer.setNamesrvAddr(NAMESRV_ADDR); + consumer.subscribe(LMQ_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; + } + }); + // use server side rebalance + consumer.setClientRebalance(false); + consumer.start(); + + // use parent topic to fill up broker addr table + consumer.getDefaultMQPushConsumerImpl().getmQClientFactory().updateTopicRouteInfoFromNameServer(TOPIC); + + final TopicRouteData topicRouteData = new TopicRouteData(); + final BrokerData brokerData = new BrokerData(); + brokerData.setCluster(CLUSTER_NAME); + brokerData.setBrokerName(BROKER_NAME); + brokerData.setBrokerAddrs(BROKER_ADDR_MAP); + topicRouteData.setBrokerDatas(Lists.newArrayList(brokerData)); + // compensate LMQ topic route for MQClientInstance#findBrokerAddrByTopic + consumer.getDefaultMQPushConsumerImpl().getmQClientFactory().getTopicRouteTable().put(LMQ_TOPIC, topicRouteData); + // re-balance immediately to start pulling messages + consumer.getDefaultMQPushConsumerImpl().getmQClientFactory().doRebalance(); + + System.out.printf("Consumer Started.%n"); + } + + private static void switchPop() throws Exception { + DefaultMQAdminExt mqAdminExt = new DefaultMQAdminExt(); + mqAdminExt.setNamesrvAddr(NAMESRV_ADDR); + 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, LMQ_TOPIC, CONSUMER_GROUP, MessageRequestMode.POP, 8, + 3_000); + } + } + } +} 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 0000000..2a7af58 --- /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 0000000..b5509d3 --- /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 0000000..f12383a --- /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 new file mode 100644 index 0000000..ec72aa0 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/openmessaging/SimpleProducer.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.openmessaging; + +import io.openmessaging.Future; +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.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) { + // You need to set the environment variable OMS_RMQ_DIRECT_NAME_SRV=true + final MessagingAccessPoint messagingAccessPoint = + OMS.getMessagingAccessPoint(URL); + + 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(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 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.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 new file mode 100644 index 0000000..9ad69b3 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/openmessaging/SimplePullConsumer.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.example.openmessaging; + +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; +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) { + // 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.newKeyValue().put(OMSBuiltinKeys.CONSUMER_ID, QUEUE)); + + messagingAccessPoint.startup(); + System.out.printf("MessagingAccessPoint startup OK%n"); + + 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"); + + // 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.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 new file mode 100644 index 0000000..92ccff1 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/openmessaging/SimplePushConsumer.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.openmessaging; + +import io.openmessaging.Message; +import io.openmessaging.MessagingAccessPoint; +import io.openmessaging.OMS; +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) { + // 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(OMSBuiltinKeys.CONSUMER_ID, "OMS_CONSUMER")); + + messagingAccessPoint.startup(); + System.out.printf("MessagingAccessPoint startup OK%n"); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + consumer.shutdown(); + messagingAccessPoint.shutdown(); + })); + + consumer.attachQueue(QUEUE, (message, 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/example/src/main/java/org/apache/rocketmq/example/operation/Consumer.java b/example/src/main/java/org/apache/rocketmq/example/operation/Consumer.java new file mode 100644 index 0000000..378a976 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/operation/Consumer.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.example.operation; + +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.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 Consumer { + + public static void main(String[] args) throws MQClientException { + CommandLine commandLine = buildCommandline(args); + if (commandLine != null) { + String subGroup = commandLine.getOptionValue('g'); + String topic = commandLine.getOptionValue('t'); + String subExpression = commandLine.getOptionValue('s'); + final String returnFailedHalf = commandLine.getOptionValue('f'); + + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(subGroup); + consumer.setInstanceName(Long.toString(System.currentTimeMillis())); + + consumer.subscribe(topic, subExpression); + + consumer.registerMessageListener(new MessageListenerConcurrently() { + AtomicLong consumeTimes = new AtomicLong(0); + + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, + ConsumeConcurrentlyContext context) { + long currentTimes = this.consumeTimes.incrementAndGet(); + System.out.printf("%-8d %s%n", currentTimes, msgs); + if (Boolean.parseBoolean(returnFailedHalf)) { + if ((currentTimes % 2) == 0) { + return ConsumeConcurrentlyStatus.RECONSUME_LATER; + } + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + + consumer.start(); + + System.out.printf("Consumer Started.%n"); + } + } + + public static CommandLine buildCommandline(String[] args) { + final Options options = new Options(); + Option opt = new Option("h", "help", false, "Print help"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("g", "consumerGroup", true, "Consumer Group Name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("t", "topic", true, "Topic Name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("s", "subscription", true, "subscription"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("f", "returnFailedHalf", true, "return failed result, for half message"); + opt.setRequired(true); + options.addOption(opt); + + DefaultParser parser = new DefaultParser(); + HelpFormatter hf = new HelpFormatter(); + hf.setWidth(110); + CommandLine commandLine = null; + try { + commandLine = parser.parse(options, args); + if (commandLine.hasOption('h')) { + hf.printHelp("producer", options, true); + return null; + } + } catch (ParseException e) { + hf.printHelp("producer", options, true); + return null; + } + + return commandLine; + } +} 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 new file mode 100644 index 0000000..0cf260d --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/operation/Producer.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.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.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 { + CommandLine commandLine = buildCommandline(args); + if (commandLine != null) { + String group = commandLine.getOptionValue('g'); + String topic = commandLine.getOptionValue('t'); + String tags = commandLine.getOptionValue('a'); + String keys = commandLine.getOptionValue('k'); + String msgCount = commandLine.getOptionValue('c'); + + DefaultMQProducer producer = new DefaultMQProducer(group); + producer.setInstanceName(Long.toString(System.currentTimeMillis())); + + producer.start(); + + for (int i = 0; i < Integer.parseInt(msgCount); i++) { + try { + Message msg = new Message( + topic, + tags, + keys, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); + SendResult sendResult = producer.send(msg); + System.out.printf("%-8d %s%n", i, sendResult); + } catch (Exception e) { + e.printStackTrace(); + Thread.sleep(1000); + } + } + + producer.shutdown(); + } + } + + public static CommandLine buildCommandline(String[] args) { + final Options options = new Options(); + Option opt = new Option("h", "help", false, "Print help"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("g", "producerGroup", true, "Producer Group Name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("t", "topic", true, "Topic Name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("a", "tags", true, "Tags Name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("k", "keys", true, "Keys Name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("c", "msgCount", true, "Message Count"); + opt.setRequired(true); + options.addOption(opt); + + DefaultParser parser = new DefaultParser(); + HelpFormatter hf = new HelpFormatter(); + hf.setWidth(110); + CommandLine commandLine = null; + try { + commandLine = parser.parse(options, args); + if (commandLine.hasOption('h')) { + hf.printHelp("producer", options, true); + return null; + } + } catch (ParseException e) { + hf.printHelp("producer", options, true); + return null; + } + + return commandLine; + } +} 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 new file mode 100644 index 0000000..257c6a0 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/ordermessage/Consumer.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.ordermessage; + +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +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.exception.MQClientException; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.MessageExt; + +public class Consumer { + + public static void main(String[] args) throws MQClientException { + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3"); + + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + + consumer.subscribe("TopicTest", "TagA || TagC || TagD"); + + consumer.registerMessageListener(new MessageListenerOrderly() { + AtomicLong consumeTimes = new AtomicLong(0); + + @Override + public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) { + 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() % 5) == 0) { + context.setSuspendCurrentQueueTimeMillis(3000); + return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT; + } + + return ConsumeOrderlyStatus.SUCCESS; + } + }); + + consumer.start(); + System.out.printf("Consumer Started.%n"); + } + +} 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 new file mode 100644 index 0000000..8ee11bf --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/ordermessage/Producer.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.example.ordermessage; + +import org.apache.rocketmq.client.exception.MQClientException; +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 org.apache.rocketmq.remoting.common.RemotingHelper; + +import java.util.List; + +public class Producer { + public static void main(String[] args) throws MQClientException { + try { + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + producer.start(); + + String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"}; + for (int i = 0; i < 100; i++) { + int orderId = i % 10; + Message msg = + new Message("TopicTestjjj", tags[i % tags.length], "KEY" + i, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); + SendResult sendResult = producer.send(msg, new MessageQueueSelector() { + @Override + public MessageQueue select(List mqs, Message msg, Object arg) { + Integer id = (Integer) arg; + int index = id % mqs.size(); + return mqs.get(index); + } + }, orderId); + + System.out.printf("%s%n", sendResult); + } + + producer.shutdown(); + } 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 new file mode 100644 index 0000000..3a101bf --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/quickstart/Consumer.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.example.quickstart; + +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; + +/** + * This example shows how to subscribe and consume messages using providing {@link DefaultMQPushConsumer}. + */ +public class Consumer { + + 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(CONSUMER_GROUP); + + /* + * Specify name server addresses. + *

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

    +         * {@code
    +         * consumer.setNamesrvAddr("name-server1-ip:9876;name-server2-ip:9876");
    +         * }
    +         * 
    + */ + // 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 specific consumer group is a brand-new one. + */ + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + + /* + * Subscribe one more topic to consume. + */ + consumer.subscribe(TOPIC, "*"); + + /* + * Register callback to execute on arrival of messages fetched from brokers. + */ + consumer.registerMessageListener((MessageListenerConcurrently) (msg, context) -> { + System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msg); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + + /* + * Launch the consumer instance. + */ + consumer.start(); + + System.out.printf("Consumer Started.%n"); + } +} 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 new file mode 100644 index 0000000..cae16ce --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/quickstart/Producer.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.example.quickstart; + +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; + +/** + * 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(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");
    +         * }
    +         * 
    + */ + // 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 < MESSAGE_COUNT; i++) { + try { + + /* + * Create a message instance, specifying topic, tag and message body. + */ + 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, 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) { + e.printStackTrace(); + Thread.sleep(1000); + } + } + + /* + * 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 0000000..31df559 --- /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 0000000..69048de --- /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 0000000..a1c18ae --- /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 0000000..b3fab65 --- /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 0000000..aeae492 --- /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 0000000..7889835 --- /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 0000000..c4e3b4f --- /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 0000000..15134a5 --- /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 new file mode 100644 index 0000000..42d19b1 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/simple/AsyncProducer.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.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; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.remoting.common.RemotingHelper; + +public class AsyncProducer { + public static void main( + String[] args) throws MQClientException, InterruptedException, UnsupportedEncodingException { + + 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); + + 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", + "TagA", + "OrderID188", + "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + 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(); + } + }); + } catch (Exception 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 new file mode 100644 index 0000000..41e28c5 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/simple/CachedQueue.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.example.simple; + +import java.util.TreeMap; +import org.apache.rocketmq.common.message.MessageExt; + +public class CachedQueue { + 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 0000000..0d8fc1c --- /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 0000000..fb673df --- /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 0000000..e5c1a61 --- /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 0000000..e4bb066 --- /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 0000000..f3edc5c --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/simple/PopConsumer.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.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 final String NAMESRV_ADDR = "127.0.0.1:9876"; + 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; + } + }); + // Uncomment the following line while debugging, namesrvAddr should be set to your local address + // consumer.setNamesrvAddr(NAMESRV_ADDR); + consumer.setClientRebalance(false); + consumer.start(); + System.out.printf("Consumer Started.%n"); + } + private static void switchPop() throws Exception { + DefaultMQAdminExt mqAdminExt = new DefaultMQAdminExt(); + // mqAdminExt.setNamesrvAddr(NAMESRV_ADDR); + 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 new file mode 100644 index 0000000..920d481 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/simple/Producer.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.simple; + +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 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(PRODUCER_GROUP); + + // 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 < 128; i++) { + try { + 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 new file mode 100644 index 0000000..e1a02aa --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/simple/PullConsumer.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.example.simple; + +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 { + + public static void main(String[] args) throws MQClientException { + + 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(); + + 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 + + } + + @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(); + } + } + } + + 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; + } + + public void incPullTPS(String topic, int pullSize) { + consumer.getDefaultMQPullConsumerImpl().getRebalanceImpl().getmQClientFactory() + .getConsumerStatsManager().incPullTPS(consumer.getConsumerGroup(), topic, pullSize); + } + }); + + } +// executors.shutdown(); +// 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 new file mode 100644 index 0000000..c652b06 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/simple/PullScheduleService.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.example.simple; + +import org.apache.rocketmq.client.consumer.MQPullConsumer; +import org.apache.rocketmq.client.consumer.MQPullConsumerScheduleService; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.consumer.PullTaskCallback; +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.remoting.protocol.heartbeat.MessageModel; + +public class PullScheduleService { + + public static void main(String[] args) throws MQClientException { + final MQPullConsumerScheduleService scheduleService = new MQPullConsumerScheduleService("GroupName1"); + + scheduleService.setMessageModel(MessageModel.CLUSTERING); + scheduleService.registerPullTaskCallback("TopicTest", new PullTaskCallback() { + + @Override + public void doPullTask(MessageQueue mq, PullTaskContext context) { + MQPullConsumer consumer = context.getPullConsumer(); + try { + + long offset = consumer.fetchConsumeOffset(mq, false); + if (offset < 0) + offset = 0; + + PullResult pullResult = consumer.pull(mq, "*", offset, 32); + System.out.printf("%s%n", offset + "\t" + mq + "\t" + pullResult); + switch (pullResult.getPullStatus()) { + case FOUND: + break; + case NO_MATCHED_MSG: + break; + case NO_NEW_MSG: + case OFFSET_ILLEGAL: + break; + default: + break; + } + consumer.updateConsumeOffset(mq, pullResult.getNextBeginOffset()); + + context.setPullNextDelayTimeMillis(100); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + + scheduleService.start(); + } +} 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 new file mode 100644 index 0000000..9de2b01 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/simple/PushConsumer.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.example.simple; + +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; + +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(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("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"); + } +} 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 new file mode 100644 index 0000000..d4bd0ea --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/simple/RandomAsyncCommit.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.simple; + +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; + +public class RandomAsyncCommit { + private final ConcurrentHashMap mqCachedTable = + new ConcurrentHashMap<>(); + + public void putMessages(final MessageQueue mq, final List msgs) { + CachedQueue cachedQueue = this.mqCachedTable.get(mq); + if (null == cachedQueue) { + cachedQueue = new CachedQueue(); + this.mqCachedTable.put(mq, cachedQueue); + } + for (MessageExt msg : msgs) { + cachedQueue.getMsgCachedTable().put(msg.getQueueOffset(), msg); + } + } + + public void removeMessage(final MessageQueue mq, long offset) { + CachedQueue cachedQueue = this.mqCachedTable.get(mq); + if (null != cachedQueue) { + cachedQueue.getMsgCachedTable().remove(offset); + } + } + + public long commitableOffset(final MessageQueue mq) { + CachedQueue cachedQueue = this.mqCachedTable.get(mq); + if (null != cachedQueue) { + return cachedQueue.getMsgCachedTable().firstKey(); + } + + return -1; + } +} 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 0000000..0c41b5b --- /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 0000000..9ac7c16 --- /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 0000000..dc05c72 --- /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 0000000..72dde67 --- /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 0000000..81c5e31 --- /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/TransactionListenerImpl.java b/example/src/main/java/org/apache/rocketmq/example/transaction/TransactionListenerImpl.java new file mode 100644 index 0000000..cb066d2 --- /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 new file mode 100644 index 0000000..d1d57c5 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/transaction/TransactionProducer.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.example.transaction; + +import org.apache.rocketmq.client.exception.MQClientException; +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.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 { + 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"}; + for (int i = 0; i < MESSAGE_COUNT; i++) { + try { + Message msg = + new Message(TOPIC, 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(); + } +} diff --git a/filter/BUILD.bazel b/filter/BUILD.bazel new file mode 100644 index 0000000..048c3bd --- /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 new file mode 100644 index 0000000..31ab74c --- /dev/null +++ b/filter/pom.xml @@ -0,0 +1,49 @@ + + + + + + rocketmq-all + org.apache.rocketmq + 5.3.4-SNAPSHOT + + + 4.0.0 + jar + rocketmq-filter + rocketmq-filter ${project.version} + + + ${basedir}/.. + + + + + ${project.groupId} + rocketmq-common + + + ${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 new file mode 100644 index 0000000..71a8b4d --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/FilterFactory.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.filter; + +import java.util.HashMap; +import java.util.Map; + +/** + * Filter factory: support other filter to register. + */ +public class FilterFactory { + + public static final FilterFactory INSTANCE = new FilterFactory(); + + protected static final Map FILTER_SPI_HOLDER = new HashMap<>(4); + + static { + FilterFactory.INSTANCE.register(new SqlFilter()); + } + + /** + * Register a filter. + *
    + * Note: + *
  • 1. Filter registered will be used in broker server, so take care of it's reliability and performance.
  • + */ + public void register(FilterSpi filterSpi) { + if (FILTER_SPI_HOLDER.containsKey(filterSpi.ofType())) { + throw new IllegalArgumentException(String.format("Filter spi type(%s) already exist!", filterSpi.ofType())); + } + + FILTER_SPI_HOLDER.put(filterSpi.ofType(), filterSpi); + } + + /** + * Un register a filter. + */ + public FilterSpi unRegister(String type) { + return FILTER_SPI_HOLDER.remove(type); + } + + /** + * Get a filter registered, null if none exist. + */ + public FilterSpi get(String type) { + return FILTER_SPI_HOLDER.get(type); + } + +} diff --git a/filter/src/main/java/org/apache/rocketmq/filter/FilterSpi.java b/filter/src/main/java/org/apache/rocketmq/filter/FilterSpi.java new file mode 100644 index 0000000..f2d9f1c --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/FilterSpi.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; + +import org.apache.rocketmq.filter.expression.Expression; +import org.apache.rocketmq.filter.expression.MQFilterException; + +/** + * Filter spi interface. + */ +public interface FilterSpi { + + /** + * Compile. + */ + Expression compile(final String expr) throws MQFilterException; + + /** + * Which type. + */ + String ofType(); +} diff --git a/filter/src/main/java/org/apache/rocketmq/filter/SqlFilter.java b/filter/src/main/java/org/apache/rocketmq/filter/SqlFilter.java new file mode 100644 index 0000000..0c1ffb8 --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/SqlFilter.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.filter; + +import org.apache.rocketmq.common.filter.ExpressionType; +import org.apache.rocketmq.filter.expression.Expression; +import org.apache.rocketmq.filter.expression.MQFilterException; +import org.apache.rocketmq.filter.parser.SelectorParser; + +/** + * SQL92 Filter, just a wrapper of {@link org.apache.rocketmq.filter.parser.SelectorParser}. + *

    + *

    + * Do not use this filter directly.Use {@link FilterFactory#get} to select a filter. + *

    + */ +public class SqlFilter implements FilterSpi { + + @Override + public Expression compile(final String expr) throws MQFilterException { + return SelectorParser.parse(expr); + } + + @Override + public String ofType() { + return ExpressionType.SQL92; + } +} diff --git a/filter/src/main/java/org/apache/rocketmq/filter/constant/UnaryType.java b/filter/src/main/java/org/apache/rocketmq/filter/constant/UnaryType.java new file mode 100644 index 0000000..d2d04cd --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/constant/UnaryType.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.filter.constant; + +public enum UnaryType { + NEGATE, + IN, + NOT, + BOOLEANCAST, + LIKE +} diff --git a/filter/src/main/java/org/apache/rocketmq/filter/expression/BinaryExpression.java b/filter/src/main/java/org/apache/rocketmq/filter/expression/BinaryExpression.java new file mode 100644 index 0000000..a3bf66b --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/expression/BinaryExpression.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.filter.expression; + +/** + * An expression which performs an operation on two expression values. + *

    + * This class was taken from ActiveMQ org.apache.activemq.filter.BinaryExpression, + *

    + */ +public abstract class BinaryExpression implements Expression { + protected Expression left; + protected Expression right; + + public BinaryExpression(Expression left, Expression right) { + this.left = left; + this.right = right; + } + + public Expression getLeft() { + return left; + } + + public Expression getRight() { + return right; + } + + /** + * @see Object#toString() + */ + public String toString() { + return "(" + left.toString() + " " + getExpressionSymbol() + " " + right.toString() + ")"; + } + + /** + * @see Object#hashCode() + */ + public int hashCode() { + return toString().hashCode(); + } + + /** + * @see Object#equals(Object) + */ + public boolean equals(Object o) { + + if (o == null || !this.getClass().equals(o.getClass())) { + return false; + } + return toString().equals(o.toString()); + + } + + /** + * Returns the symbol that represents this binary expression. For example, addition is + * represented by "+" + */ + public abstract String getExpressionSymbol(); + + /** + * @param expression + */ + public void setRight(Expression expression) { + right = expression; + } + + /** + * @param expression + */ + public void setLeft(Expression expression) { + left = expression; + } + +} 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 0000000..958e622 --- /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/BooleanExpression.java b/filter/src/main/java/org/apache/rocketmq/filter/expression/BooleanExpression.java new file mode 100644 index 0000000..068b362 --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/expression/BooleanExpression.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; + +/** + * A BooleanExpression is an expression that always + * produces a Boolean result. + *

    + * This class was taken from ActiveMQ org.apache.activemq.filter.BooleanExpression, + * but the parameter is changed to an interface. + *

    + * + * @see org.apache.rocketmq.filter.expression.EvaluationContext + */ +public interface BooleanExpression extends Expression { + + /** + * @return true if the expression evaluates to Boolean.TRUE. + */ + boolean matches(EvaluationContext context) throws Exception; + +} 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 new file mode 100644 index 0000000..14fd704 --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/expression/ComparisonExpression.java @@ -0,0 +1,661 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; + +import java.util.List; + +/** + * A filter performing a comparison of two objects + *

    + * This class was taken from ActiveMQ org.apache.activemq.filter.ComparisonExpression, + * but: + * 1. Remove LIKE expression, and related methods; + * 2. Extract a new method __compare which has int return value; + * 3. When create between expression, check whether left value is less or equal than right value; + * 4. For string type value(can not convert to number), only equal or unequal comparison are supported. + *

    + */ +public abstract class ComparisonExpression extends BinaryExpression implements BooleanExpression { + + public static final ThreadLocal CONVERT_STRING_EXPRESSIONS = new ThreadLocal<>(); + + boolean convertStringExpressions = false; + + /** + * @param left + * @param right + */ + public ComparisonExpression(Expression left, Expression right) { + super(left, right); + convertStringExpressions = CONVERT_STRING_EXPRESSIONS.get() != null; + } + + public static BooleanExpression createBetween(Expression value, Expression left, Expression right) { + // check + if (left instanceof ConstantExpression && right instanceof ConstantExpression) { + Object lv = ((ConstantExpression) left).getValue(); + Object rv = ((ConstantExpression) right).getValue(); + if (lv == null || rv == null) { + throw new RuntimeException("Illegal values of between, values can not be null!"); + } + if (lv instanceof Comparable && rv instanceof Comparable) { + int ret = __compare((Comparable) rv, (Comparable) lv, true); + if (ret < 0) + throw new RuntimeException( + String.format("Illegal values of between, left value(%s) must less than or equal to right value(%s)", lv, rv) + ); + } + } + + return LogicExpression.createAND(createGreaterThanEqual(value, left), createLessThanEqual(value, right)); + } + + public static BooleanExpression createNotBetween(Expression value, Expression left, Expression right) { + 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) { + + if (!(left instanceof PropertyExpression)) { + throw new RuntimeException("Expected a property for In expression, got: " + left); + } + return UnaryExpression.createInExpression((PropertyExpression) left, elements, false); + + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static BooleanExpression createNotInFilter(Expression left, List elements) { + + if (!(left instanceof PropertyExpression)) { + throw new RuntimeException("Expected a property for In expression, got: " + left); + } + return UnaryExpression.createInExpression((PropertyExpression) left, elements, true); + + } + + public static BooleanExpression createIsNull(Expression left) { + return doCreateEqual(left, BooleanConstantExpression.NULL); + } + + public static BooleanExpression createIsNotNull(Expression left) { + return UnaryExpression.createNOT(doCreateEqual(left, BooleanConstantExpression.NULL)); + } + + public static BooleanExpression createNotEqual(Expression left, Expression right) { + return UnaryExpression.createNOT(createEqual(left, right)); + } + + public static BooleanExpression createEqual(Expression left, Expression right) { + checkEqualOperand(left); + checkEqualOperand(right); + checkEqualOperandCompatability(left, right); + return doCreateEqual(left, right); + } + + @SuppressWarnings({"rawtypes"}) + private static BooleanExpression doCreateEqual(Expression left, Expression right) { + return new ComparisonExpression(left, right) { + + public Object evaluate(EvaluationContext context) throws Exception { + Object lv = left.evaluate(context); + Object rv = right.evaluate(context); + + // If one of the values is null + if (lv == null ^ rv == null) { + if (lv == null) { + return null; + } + return Boolean.FALSE; + } + if (lv == rv || lv.equals(rv)) { + return Boolean.TRUE; + } + if (lv instanceof Comparable && rv instanceof Comparable) { + return compare((Comparable) lv, (Comparable) rv); + } + return Boolean.FALSE; + } + + protected boolean asBoolean(int answer) { + return answer == 0; + } + + public String getExpressionSymbol() { + return "=="; + } + }; + } + + public static BooleanExpression createGreaterThan(final Expression left, final Expression right) { + checkLessThanOperand(left); + checkLessThanOperand(right); + return new ComparisonExpression(left, right) { + protected boolean asBoolean(int answer) { + return answer > 0; + } + + public String getExpressionSymbol() { + return ">"; + } + }; + } + + public static BooleanExpression createGreaterThanEqual(final Expression left, final Expression right) { + checkLessThanOperand(left); + checkLessThanOperand(right); + return new ComparisonExpression(left, right) { + protected boolean asBoolean(int answer) { + return answer >= 0; + } + + public String getExpressionSymbol() { + return ">="; + } + }; + } + + public static BooleanExpression createLessThan(final Expression left, final Expression right) { + checkLessThanOperand(left); + checkLessThanOperand(right); + return new ComparisonExpression(left, right) { + + protected boolean asBoolean(int answer) { + return answer < 0; + } + + public String getExpressionSymbol() { + return "<"; + } + + }; + } + + public static BooleanExpression createLessThanEqual(final Expression left, final Expression right) { + checkLessThanOperand(left); + checkLessThanOperand(right); + return new ComparisonExpression(left, right) { + + protected boolean asBoolean(int answer) { + return answer <= 0; + } + + public String getExpressionSymbol() { + return "<="; + } + }; + } + + /** + * Only Numeric expressions can be used in >, >=, < or <= expressions.s + */ + public static void checkLessThanOperand(Expression expr) { + if (expr instanceof ConstantExpression) { + Object value = ((ConstantExpression) expr).getValue(); + if (value instanceof Number) { + return; + } + + // Else it's boolean or a String.. + throw new RuntimeException("Value '" + expr + "' cannot be compared."); + } + if (expr instanceof BooleanExpression) { + throw new RuntimeException("Value '" + expr + "' cannot be compared."); + } + } + + /** + * Validates that the expression can be used in == or <> expression. Cannot + * not be NULL TRUE or FALSE litterals. + */ + public static void checkEqualOperand(Expression expr) { + if (expr instanceof ConstantExpression) { + Object value = ((ConstantExpression) expr).getValue(); + if (value == null) { + throw new RuntimeException("'" + expr + "' cannot be compared."); + } + } + } + + /** + * @param left + * @param right + */ + private static void checkEqualOperandCompatability(Expression left, Expression right) { + if (left instanceof ConstantExpression && right instanceof ConstantExpression) { + if (left instanceof BooleanExpression && !(right instanceof BooleanExpression)) { + throw new RuntimeException("'" + left + "' cannot be compared with '" + right + "'"); + } + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public Object evaluate(EvaluationContext context) throws Exception { + Comparable lv = (Comparable) left.evaluate(context); + if (lv == null) { + return null; + } + Comparable rv = (Comparable) right.evaluate(context); + if (rv == null) { + return null; + } + if (getExpressionSymbol().equals(">=") || getExpressionSymbol().equals(">") + || getExpressionSymbol().equals("<") || getExpressionSymbol().equals("<=")) { + Class lc = lv.getClass(); + Class rc = rv.getClass(); + if (lc == rc && lc == String.class) { + // Compare String is illegal + // first try to convert to double + try { + Comparable lvC = Double.valueOf((String) (Comparable) lv); + Comparable rvC = Double.valueOf((String) rv); + + return compare(lvC, rvC); + } catch (Exception e) { + throw new RuntimeException("It's illegal to compare string by '>=', '>', '<', '<='. lv=" + lv + ", rv=" + rv, e); + } + } + } + return compare(lv, rv); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + protected static int __compare(Comparable lv, Comparable rv, boolean convertStringExpressions) { + Class lc = lv.getClass(); + Class rc = rv.getClass(); + // If the the objects are not of the same type, + // try to convert up to allow the comparison. + if (lc != rc) { + try { + if (lc == Boolean.class) { + if (convertStringExpressions && rc == String.class) { + lv = Boolean.valueOf((String) lv).booleanValue(); + } else { + return -1; + } + } else if (lc == Byte.class) { + if (rc == Short.class) { + lv = Short.valueOf(((Number) lv).shortValue()); + } else if (rc == Integer.class) { + lv = Integer.valueOf(((Number) lv).intValue()); + } else if (rc == Long.class) { + lv = Long.valueOf(((Number) lv).longValue()); + } else if (rc == Float.class) { + lv = new Float(((Number) lv).floatValue()); + } else if (rc == Double.class) { + lv = new Double(((Number) lv).doubleValue()); + } else if (convertStringExpressions && rc == String.class) { + rv = Byte.valueOf((String) rv); + } else { + return -1; + } + } else if (lc == Short.class) { + if (rc == Integer.class) { + lv = Integer.valueOf(((Number) lv).intValue()); + } else if (rc == Long.class) { + lv = Long.valueOf(((Number) lv).longValue()); + } else if (rc == Float.class) { + lv = new Float(((Number) lv).floatValue()); + } else if (rc == Double.class) { + lv = new Double(((Number) lv).doubleValue()); + } else if (convertStringExpressions && rc == String.class) { + rv = Short.valueOf((String) rv); + } else { + return -1; + } + } else if (lc == Integer.class) { + if (rc == Long.class) { + lv = Long.valueOf(((Number) lv).longValue()); + } else if (rc == Float.class) { + lv = new Float(((Number) lv).floatValue()); + } else if (rc == Double.class) { + lv = new Double(((Number) lv).doubleValue()); + } else if (convertStringExpressions && rc == String.class) { + rv = Integer.valueOf((String) rv); + } else { + return -1; + } + } else if (lc == Long.class) { + if (rc == Integer.class) { + rv = Long.valueOf(((Number) rv).longValue()); + } else if (rc == Float.class) { + lv = new Float(((Number) lv).floatValue()); + } else if (rc == Double.class) { + lv = new Double(((Number) lv).doubleValue()); + } else if (convertStringExpressions && rc == String.class) { + rv = Long.valueOf((String) rv); + } else { + return -1; + } + } else if (lc == Float.class) { + if (rc == Integer.class) { + rv = new Float(((Number) rv).floatValue()); + } else if (rc == Long.class) { + rv = new Float(((Number) rv).floatValue()); + } else if (rc == Double.class) { + lv = new Double(((Number) lv).doubleValue()); + } else if (convertStringExpressions && rc == String.class) { + rv = Float.valueOf((String) rv); + } else { + return -1; + } + } else if (lc == Double.class) { + if (rc == Integer.class) { + rv = new Double(((Number) rv).doubleValue()); + } else if (rc == Long.class) { + rv = new Double(((Number) rv).doubleValue()); + } else if (rc == Float.class) { + rv = new Float(((Number) rv).doubleValue()); + } else if (convertStringExpressions && rc == String.class) { + rv = Double.valueOf((String) rv); + } else { + return -1; + } + } else if (convertStringExpressions && lc == String.class) { + if (rc == Boolean.class) { + lv = Boolean.valueOf((String) lv); + } else if (rc == Byte.class) { + lv = Byte.valueOf((String) lv); + } else if (rc == Short.class) { + lv = Short.valueOf((String) lv); + } else if (rc == Integer.class) { + lv = Integer.valueOf((String) lv); + } else if (rc == Long.class) { + lv = Long.valueOf((String) lv); + } else if (rc == Float.class) { + lv = Float.valueOf((String) lv); + } else if (rc == Double.class) { + lv = Double.valueOf((String) lv); + } else { + return -1; + } + } else { + return -1; + } + } catch (NumberFormatException e) { + throw new RuntimeException(e); + } + } + return lv.compareTo(rv); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + protected Boolean compare(Comparable lv, Comparable rv) { + return asBoolean(__compare(lv, rv, convertStringExpressions)) ? Boolean.TRUE : Boolean.FALSE; + } + + protected abstract boolean asBoolean(int answer); + + 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/ConstantExpression.java b/filter/src/main/java/org/apache/rocketmq/filter/expression/ConstantExpression.java new file mode 100644 index 0000000..127bd8b --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/expression/ConstantExpression.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.filter.expression; + +/** + * Represents a constant expression + *

    + * This class was taken from ActiveMQ org.apache.activemq.filter.ConstantExpression, + * but: + * 1. For long type constant, the range bound by java Long type; + * 2. For float type constant, the range bound by java Double type; + * 3. Remove Hex and Octal expression; + * 4. Add now expression to support to get current time. + *

    + */ +public class ConstantExpression implements Expression { + + private Object value; + + public ConstantExpression(Object value) { + this.value = value; + } + + public static ConstantExpression createFromDecimal(String text) { + + // Strip off the 'l' or 'L' if needed. + if (text.endsWith("l") || text.endsWith("L")) { + text = text.substring(0, text.length() - 1); + } + + // only support Long.MIN_VALUE ~ Long.MAX_VALUE + Number value = new Long(text); + + long l = value.longValue(); + if (Integer.MIN_VALUE <= l && l <= Integer.MAX_VALUE) { + value = value.intValue(); + } + return new ConstantExpression(value); + } + + public static ConstantExpression createFloat(String text) { + Double value = new Double(text); + if (value > Double.MAX_VALUE) { + throw new RuntimeException(text + " is greater than " + Double.MAX_VALUE); + } + if (value < Double.MIN_VALUE) { + throw new RuntimeException(text + " is less than " + Double.MIN_VALUE); + } + return new ConstantExpression(value); + } + + public static ConstantExpression createNow() { + return new NowExpression(); + } + + public Object evaluate(EvaluationContext context) throws Exception { + return value; + } + + public Object getValue() { + return value; + } + + /** + * @see Object#toString() + */ + public String toString() { + Object value = getValue(); + if (value == null) { + return "NULL"; + } + if (value instanceof Boolean) { + return (Boolean) value ? "TRUE" : "FALSE"; + } + if (value instanceof String) { + return encodeString((String) value); + } + return value.toString(); + } + + /** + * @see Object#hashCode() + */ + public int hashCode() { + return toString().hashCode(); + } + + /** + * @see Object#equals(Object) + */ + public boolean equals(Object o) { + + if (o == null || !this.getClass().equals(o.getClass())) { + return false; + } + return toString().equals(o.toString()); + + } + + /** + * Encodes the value of string so that it looks like it would look like when + * it was provided in a selector. + */ + public static String encodeString(String s) { + + StringBuilder builder = new StringBuilder(); + + builder.append('\''); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\'') { + builder.append(c); + } + builder.append(c); + } + builder.append('\''); + return builder.toString(); + } + +} diff --git a/filter/src/main/java/org/apache/rocketmq/filter/expression/EmptyEvaluationContext.java b/filter/src/main/java/org/apache/rocketmq/filter/expression/EmptyEvaluationContext.java new file mode 100644 index 0000000..52af2d0 --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/expression/EmptyEvaluationContext.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.filter.expression; + +import java.util.Map; + +/** + * Empty context. + */ +public class EmptyEvaluationContext implements EvaluationContext { + @Override + public Object get(String name) { + return null; + } + + @Override + public Map keyValues() { + return null; + } +} diff --git a/filter/src/main/java/org/apache/rocketmq/filter/expression/EvaluationContext.java b/filter/src/main/java/org/apache/rocketmq/filter/expression/EvaluationContext.java new file mode 100644 index 0000000..1bea15b --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/expression/EvaluationContext.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.filter.expression; + +import java.util.Map; + +/** + * Context of evaluate expression. + * + * Compare to org.apache.activemq.filter.MessageEvaluationContext, this is just an interface. + */ +public interface EvaluationContext { + + /** + * Get value by name from context + */ + Object get(String name); + + /** + * Context variables. + */ + Map keyValues(); +} diff --git a/filter/src/main/java/org/apache/rocketmq/filter/expression/Expression.java b/filter/src/main/java/org/apache/rocketmq/filter/expression/Expression.java new file mode 100644 index 0000000..3e6d9b3 --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/expression/Expression.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.filter.expression; + +/** + * Interface of expression. + *

    + * This class was taken from ActiveMQ org.apache.activemq.filter.Expression, + * but the parameter is changed to an interface. + *

    + * + * @see org.apache.rocketmq.filter.expression.EvaluationContext + */ +public interface Expression { + + /** + * Calculate express result with context. + * + * @param context context of evaluation + * @return the value of this expression + */ + Object evaluate(EvaluationContext context) throws Exception; +} diff --git a/filter/src/main/java/org/apache/rocketmq/filter/expression/LogicExpression.java b/filter/src/main/java/org/apache/rocketmq/filter/expression/LogicExpression.java new file mode 100644 index 0000000..1062bb8 --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/expression/LogicExpression.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.filter.expression; + +/** + * A filter performing a comparison of two objects + *

    + * This class was taken from ActiveMQ org.apache.activemq.filter.LogicExpression, + *

    + */ +public abstract class LogicExpression extends BinaryExpression implements BooleanExpression { + + /** + * @param left + * @param right + */ + public LogicExpression(BooleanExpression left, BooleanExpression right) { + super(left, right); + } + + public static BooleanExpression createOR(BooleanExpression lvalue, BooleanExpression rvalue) { + return new LogicExpression(lvalue, rvalue) { + + public Object evaluate(EvaluationContext context) throws Exception { + + Boolean lv = (Boolean) left.evaluate(context); + if (lv != null && lv.booleanValue()) { + return Boolean.TRUE; + } + Boolean rv = (Boolean) right.evaluate(context); + if (rv != null && rv.booleanValue()) { + return Boolean.TRUE; + } + if (lv == null || rv == null) { + return null; + } + return Boolean.FALSE; + } + + public String getExpressionSymbol() { + return "||"; + } + }; + } + + public static BooleanExpression createAND(BooleanExpression lvalue, BooleanExpression rvalue) { + return new LogicExpression(lvalue, rvalue) { + + public Object evaluate(EvaluationContext context) throws Exception { + + Boolean lv = (Boolean) left.evaluate(context); + + if (lv != null && !lv.booleanValue()) { + return Boolean.FALSE; + } + Boolean rv = (Boolean) right.evaluate(context); + if (rv != null && !rv.booleanValue()) { + return Boolean.FALSE; + } + if (lv == null || rv == null) { + return null; + } + return Boolean.TRUE; + } + + public String getExpressionSymbol() { + return "&&"; + } + }; + } + + public abstract Object evaluate(EvaluationContext context) throws Exception; + + 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/MQFilterException.java b/filter/src/main/java/org/apache/rocketmq/filter/expression/MQFilterException.java new file mode 100644 index 0000000..676a17b --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/expression/MQFilterException.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.filter.expression; + +/** + * Exception. + */ +public class MQFilterException extends Exception { + private static final long serialVersionUID = 1L; + private final int responseCode; + private final String errorMessage; + + public MQFilterException(String errorMessage, Throwable cause) { + super(cause); + this.responseCode = -1; + this.errorMessage = errorMessage; + } + + public MQFilterException(int responseCode, String errorMessage) { + this.responseCode = responseCode; + this.errorMessage = errorMessage; + } + + public int getResponseCode() { + return responseCode; + } + + public String getErrorMessage() { + return errorMessage; + } +} diff --git a/filter/src/main/java/org/apache/rocketmq/filter/expression/NowExpression.java b/filter/src/main/java/org/apache/rocketmq/filter/expression/NowExpression.java new file mode 100644 index 0000000..d76caca --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/expression/NowExpression.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.filter.expression; + +/** + * Current time expression.Just for test. + */ +public class NowExpression extends ConstantExpression { + public NowExpression() { + super("now"); + } + + @Override + public Object evaluate(EvaluationContext context) throws Exception { + return new Long(System.currentTimeMillis()); + } + + public Object getValue() { + return new Long(System.currentTimeMillis()); + } +} diff --git a/filter/src/main/java/org/apache/rocketmq/filter/expression/PropertyExpression.java b/filter/src/main/java/org/apache/rocketmq/filter/expression/PropertyExpression.java new file mode 100644 index 0000000..b9657b0 --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/expression/PropertyExpression.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.filter.expression; + +/** + * Represents a property expression + *

    + * This class was taken from ActiveMQ org.apache.activemq.filter.PropertyExpression, + * but more simple and no transfer between expression and message property. + *

    + */ +public class PropertyExpression implements Expression { + private final String name; + + public PropertyExpression(String name) { + this.name = name; + } + + @Override + public Object evaluate(EvaluationContext context) throws Exception { + return context.get(name); + } + + public String getName() { + return name; + } + + /** + * @see Object#toString() + */ + @Override + public String toString() { + return name; + } + + /** + * @see Object#hashCode() + */ + @Override + public int hashCode() { + return name.hashCode(); + } + + /** + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object o) { + + if (o == null || !this.getClass().equals(o.getClass())) { + return false; + } + return name.equals(((PropertyExpression) o).name); + } +} 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 new file mode 100644 index 0000000..7a62624 --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/expression/UnaryExpression.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.filter.expression; + +import org.apache.rocketmq.filter.constant.UnaryType; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; + +/** + * An expression which performs an operation on two expression values + *

    + * This class was taken from ActiveMQ org.apache.activemq.filter.UnaryExpression, + * but: + * 1. remove XPath and XQuery expression; + * 2. Add constant UnaryType to distinguish different unary expression; + * 3. Extract UnaryInExpression to an independent class. + *

    + */ +public abstract class UnaryExpression implements Expression { + + private static final BigDecimal BD_LONG_MIN_VALUE = BigDecimal.valueOf(Long.MIN_VALUE); + protected Expression right; + + public UnaryType unaryType; + + public UnaryExpression(Expression left) { + this.right = left; + } + + public UnaryExpression(Expression left, UnaryType unaryType) { + this.setUnaryType(unaryType); + this.right = left; + } + + 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) { + return null; + } + if (rvalue instanceof Number) { + return negate((Number) rvalue); + } + return null; + } + + @Override + public String getExpressionSymbol() { + return "-"; + } + }; + } + + public static BooleanExpression createInExpression(PropertyExpression right, List elements, + final boolean not) { + + // Use a HashSet if there are many elements. + Collection t; + if (elements.size() == 0) { + t = null; + } else if (elements.size() < 5) { + t = elements; + } else { + 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); + if (rvalue == null) { + return null; + } + if (rvalue.getClass() != String.class) { + return null; + } + + if ((inList != null && inList.contains(rvalue)) ^ not) { + return Boolean.TRUE; + } else { + return Boolean.FALSE; + } + + } + + @Override + public String toString() { + StringBuilder answer = new StringBuilder(); + answer.append(right); + answer.append(" "); + answer.append(getExpressionSymbol()); + answer.append(" ( "); + + int count = 0; + for (Iterator i = inList.iterator(); i.hasNext(); ) { + Object o = (Object) i.next(); + if (count != 0) { + answer.append(", "); + } + answer.append(o); + count++; + } + + answer.append(" )"); + return answer.toString(); + } + + @Override + public String getExpressionSymbol() { + if (not) { + return "NOT IN"; + } else { + return "IN"; + } + } + }; + } + + abstract static class BooleanUnaryExpression extends UnaryExpression implements BooleanExpression { + 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; + } + } + + 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) { + return null; + } + return lvalue.booleanValue() ? Boolean.FALSE : Boolean.TRUE; + } + + @Override + public String getExpressionSymbol() { + return "NOT"; + } + }; + } + + 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) { + return null; + } + if (!rvalue.getClass().equals(Boolean.class)) { + return Boolean.FALSE; + } + return ((Boolean) rvalue).booleanValue() ? Boolean.TRUE : Boolean.FALSE; + } + + @Override + public String toString() { + return right.toString(); + } + + @Override + public String getExpressionSymbol() { + return ""; + } + }; + } + + private static Number negate(Number left) { + Class clazz = left.getClass(); + if (clazz == Integer.class) { + return new Integer(-left.intValue()); + } else if (clazz == Long.class) { + return new Long(-left.longValue()); + } else if (clazz == Float.class) { + return new Float(-left.floatValue()); + } else if (clazz == Double.class) { + return new Double(-left.doubleValue()); + } else if (clazz == BigDecimal.class) { + // We ussually get a big deciamal when we have Long.MIN_VALUE + // constant in the + // Selector. Long.MIN_VALUE is too big to store in a Long as a + // positive so we store it + // as a Big decimal. But it gets Negated right away.. to here we try + // to covert it back + // to a Long. + BigDecimal bd = (BigDecimal) left; + bd = bd.negate(); + + if (BD_LONG_MIN_VALUE.compareTo(bd) == 0) { + return Long.valueOf(Long.MIN_VALUE); + } + return bd; + } else { + throw new RuntimeException("Don't know how to negate: " + left); + } + } + + public Expression getRight() { + return right; + } + + public void setRight(Expression expression) { + right = expression; + } + + public UnaryType getUnaryType() { + return unaryType; + } + + public void setUnaryType(UnaryType unaryType) { + this.unaryType = unaryType; + } + + /** + * @see Object#toString() + */ + @Override + public String toString() { + return "(" + getExpressionSymbol() + " " + right.toString() + ")"; + } + + /** + * @see Object#hashCode() + */ + @Override + public int hashCode() { + return toString().hashCode(); + } + + /** + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object o) { + + if (o == null || !this.getClass().equals(o.getClass())) { + return false; + } + return toString().equals(o.toString()); + + } + + /** + * Returns the symbol that represents this binary expression. For example, + * addition is represented by "+" + */ + public abstract String getExpressionSymbol(); + +} diff --git a/filter/src/main/java/org/apache/rocketmq/filter/expression/UnaryInExpression.java b/filter/src/main/java/org/apache/rocketmq/filter/expression/UnaryInExpression.java new file mode 100644 index 0000000..653cd92 --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/expression/UnaryInExpression.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.filter.expression; + +import org.apache.rocketmq.filter.constant.UnaryType; + +import java.util.Collection; + +/** + * In expression. + */ +abstract public class UnaryInExpression extends UnaryExpression implements BooleanExpression { + + private boolean not; + + private Collection inList; + + public UnaryInExpression(Expression left, UnaryType unaryType, + Collection inList, boolean not) { + super(left, unaryType); + this.setInList(inList); + this.setNot(not); + + } + + public boolean matches(EvaluationContext context) throws Exception { + Object object = evaluate(context); + return object != null && object == Boolean.TRUE; + } + + public boolean isNot() { + return not; + } + + public void setNot(boolean not) { + this.not = not; + } + + public Collection getInList() { + return inList; + } + + public void setInList(Collection inList) { + this.inList = inList; + } +} 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 new file mode 100644 index 0000000..3976250 --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/parser/ParseException.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. + */ + +/* Generated By:JavaCC: Do not edit this line. ParseException.java Version 5.0 */ +/* JavaCCOptions:KEEP_LINE_COL=null */ +package org.apache.rocketmq.filter.parser; + +/** + * This exception is thrown when parse errors are encountered. + * You can explicitly create objects of this exception type by + * calling the method generateParseException in the generated + * parser. + *

    + * You can modify this class to customize your error reporting + * mechanisms so long as you retain the public fields. + */ +public class ParseException extends Exception { + + /** + * The version identifier for this Serializable class. + * Increment only if the serialized form of the + * class changes. + */ + private static final long serialVersionUID = 1L; + + /** + * This constructor is used by the method "generateParseException" + * in the generated parser. Calling this constructor generates + * a new object of this type with the fields "currentToken", + * "expectedTokenSequences", and "TOKEN_IMAGE" set. + */ + public ParseException(Token currentTokenVal, + int[][] expectedTokenSequencesVal, + String[] tokenImageVal + ) { + super(initialise(currentTokenVal, expectedTokenSequencesVal, tokenImageVal)); + currentToken = currentTokenVal; + expectedTokenSequences = expectedTokenSequencesVal; + tokenImage = tokenImageVal; + } + + /** + * The following constructors are for use by you for whatever + * purpose you can think of. Constructing the exception in this + * manner makes the exception behave in the normal way - i.e., as + * documented in the class "Throwable". The fields "errorToken", + * "expectedTokenSequences", and "TOKEN_IMAGE" do not contain + * relevant information. The JavaCC generated code does not use + * these constructors. + */ + + public ParseException() { + super(); + } + + /** + * Constructor with message. + */ + public ParseException(String message) { + super(message); + } + + /** + * This is the last token that has been consumed successfully. If + * this object has been created due to a parse error, the token + * followng this token will (therefore) be the first error token. + */ + public Token currentToken; + + /** + * Each entry in this array is an array of integers. Each array + * of integers represents a sequence of tokens (by their ordinal + * values) that is expected at this point of the parse. + */ + public int[][] expectedTokenSequences; + + /** + * This is a reference to the "TOKEN_IMAGE" array of the generated + * parser within which the parse error occurred. This array is + * defined in the generated ...Constants interface. + */ + public String[] tokenImage; + + /** + * It uses "currentToken" and "expectedTokenSequences" to generate a parse + * error message and returns it. If this object has been created + * due to a parse error, and you do not catch it (it gets thrown + * from the parser) the correct error message + * gets displayed. + */ + private static String initialise(Token currentToken, + int[][] expectedTokenSequences, + String[] tokenImage) { + String eol = System.getProperty("line.separator", "\n"); + StringBuilder expected = new StringBuilder(); + int maxSize = 0; + for (int i = 0; i < expectedTokenSequences.length; i++) { + if (maxSize < expectedTokenSequences[i].length) { + maxSize = expectedTokenSequences[i].length; + } + for (int j = 0; j < expectedTokenSequences[i].length; j++) { + expected.append(tokenImage[expectedTokenSequences[i][j]]).append(' '); + } + if (expectedTokenSequences[i][expectedTokenSequences[i].length - 1] != 0) { + expected.append("..."); + } + expected.append(eol).append(" "); + } + String retval = "Encountered \""; + Token tok = currentToken.next; + for (int i = 0; i < maxSize; i++) { + if (i != 0) { + retval += " "; + } + if (tok.kind == 0) { + retval += tokenImage[0]; + break; + } + retval += " " + tokenImage[tok.kind]; + retval += " \""; + retval += add_escapes(tok.image); + retval += " \""; + tok = tok.next; + } + retval += "\" at line " + currentToken.next.beginLine + ", column " + currentToken.next.beginColumn; + retval += "." + eol; + if (expectedTokenSequences.length == 1) { + retval += "Was expecting:" + eol + " "; + } else { + retval += "Was expecting one of:" + eol + " "; + } + retval += expected.toString(); + return retval; + } + + /** + * The end of line string for this machine. + */ + protected String eol = System.getProperty("line.separator", "\n"); + + /** + * Used to convert raw characters to their escaped version + * when these raw version cannot be used as part of an ASCII + * string literal. + */ + static String add_escapes(String str) { + StringBuilder retval = new StringBuilder(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) { + case 0: + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + +} +/* 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 new file mode 100644 index 0000000..7b44aa2 --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/parser/SelectorParser.java @@ -0,0 +1,1356 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Generated By:JavaCC: Do not edit this line. SelectorParser.java */ +package org.apache.rocketmq.filter.parser; + +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; +import org.apache.rocketmq.filter.expression.Expression; +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 java.io.StringReader; +import java.util.ArrayList; + +/** + * JMS Selector Parser generated by JavaCC + *

    + * Do not edit this .java file directly - it is autogenerated from SelectorParser.jj + */ +public class SelectorParser implements SelectorParserConstants { + + private static final Cache PARSE_CACHE = CacheBuilder.newBuilder().maximumSize(100).build(); + + public static BooleanExpression parse(String sql) throws MQFilterException { + Object result = PARSE_CACHE.getIfPresent(sql); + if (result instanceof MQFilterException) { + throw (MQFilterException) result; + } else if (result instanceof BooleanExpression) { + return (BooleanExpression) result; + } else { + ComparisonExpression.CONVERT_STRING_EXPRESSIONS.set(true); + try { + + BooleanExpression e = new SelectorParser(sql).parse(); + PARSE_CACHE.put(sql, e); + return e; + } catch (MQFilterException t) { + PARSE_CACHE.put(sql, t); + throw t; + } finally { + ComparisonExpression.CONVERT_STRING_EXPRESSIONS.remove(); + } + } + } + + public static void clearCache() { + PARSE_CACHE.cleanUp(); + } + + private String sql; + + protected SelectorParser(String sql) { + this(new StringReader(sql)); + this.sql = sql; + } + + protected BooleanExpression parse() throws MQFilterException { + try { + return this.JmsSelector(); + } catch (Throwable e) { + throw new MQFilterException("Invalid MessageSelector. ", e); + } + } + + private BooleanExpression asBooleanExpression(Expression value) throws ParseException { + if (value instanceof BooleanExpression) { + return (BooleanExpression) value; + } + if (value instanceof PropertyExpression) { + return UnaryExpression.createBooleanCast(value); + } + throw new ParseException("Expression will not result in a boolean value: " + value); + } + + // ---------------------------------------------------------------------------- + // Grammar + // ---------------------------------------------------------------------------- + final public BooleanExpression JmsSelector() throws ParseException { + Expression left = orExpression(); + return asBooleanExpression(left); + } + + final public Expression orExpression() throws ParseException { + Expression left; + Expression right; + left = andExpression(); + label_1: + while (true) { + switch ((jjNtk == -1) ? jj_ntk() : jjNtk) { + case OR: + break; + default: + jjLa1[0] = jjGen; + break label_1; + } + jj_consume_token(OR); + right = andExpression(); + left = LogicExpression.createOR(asBooleanExpression(left), asBooleanExpression(right)); + } + return left; + } + + final public Expression andExpression() throws ParseException { + Expression left; + Expression right; + left = equalityExpression(); + label_2: + while (true) { + switch ((jjNtk == -1) ? jj_ntk() : jjNtk) { + case AND: + break; + default: + jjLa1[1] = jjGen; + break label_2; + } + jj_consume_token(AND); + right = equalityExpression(); + left = LogicExpression.createAND(asBooleanExpression(left), asBooleanExpression(right)); + } + return left; + } + + final public Expression equalityExpression() throws ParseException { + Expression left; + Expression right; + left = comparisonExpression(); + label_3: + while (true) { + switch ((jjNtk == -1) ? jj_ntk() : jjNtk) { + case IS: + case 25: + case 26: + break; + default: + jjLa1[2] = jjGen; + break label_3; + } + switch ((jjNtk == -1) ? jj_ntk() : jjNtk) { + case 25: + jj_consume_token(25); + right = comparisonExpression(); + left = ComparisonExpression.createEqual(left, right); + break; + case 26: + jj_consume_token(26); + right = comparisonExpression(); + left = ComparisonExpression.createNotEqual(left, right); + break; + default: + jjLa1[3] = jjGen; + if (jj_2_1(2)) { + jj_consume_token(IS); + jj_consume_token(NULL); + left = ComparisonExpression.createIsNull(left); + } else { + switch ((jjNtk == -1) ? jj_ntk() : jjNtk) { + case IS: + jj_consume_token(IS); + jj_consume_token(NOT); + jj_consume_token(NULL); + left = ComparisonExpression.createIsNotNull(left); + break; + default: + jjLa1[4] = jjGen; + jj_consume_token(-1); + throw new ParseException(); + } + } + } + } + return left; + } + + final public Expression comparisonExpression() throws ParseException { + Expression left; + Expression right; + Expression low; + Expression high; + String t, u; + boolean not; + ArrayList list; + left = unaryExpr(); + label_4: + while (true) { + switch ((jjNtk == -1) ? jj_ntk() : jjNtk) { + case NOT: + case BETWEEN: + case IN: + 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 27: + jj_consume_token(27); + right = unaryExpr(); + left = ComparisonExpression.createGreaterThan(left, right); + break; + case 28: + jj_consume_token(28); + right = unaryExpr(); + left = ComparisonExpression.createGreaterThanEqual(left, right); + break; + case 29: + jj_consume_token(29); + right = unaryExpr(); + left = ComparisonExpression.createLessThan(left, right); + break; + case 30: + jj_consume_token(30); + right = unaryExpr(); + left = ComparisonExpression.createLessThanEqual(left, right); + break; + 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(CONTAINS); + t = stringLitteral(); + left = ComparisonExpression.createNotContains(left, t); + } else { + switch ((jjNtk == -1) ? jj_ntk() : jjNtk) { + case STARTSWITH: + jj_consume_token(STARTSWITH); + t = stringLitteral(); + left = ComparisonExpression.createStartsWith(left, t); + break; + default: + jjLa1[9] = jjGen; + if (jj_2_3(2)) { + jj_consume_token(NOT); + jj_consume_token(STARTSWITH); + t = stringLitteral(); + left = ComparisonExpression.createNotStartsWith(left, t); + } else { + 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(); + } + } + } + } + } + } + } + } + } + } + } + return left; + } + + final public Expression unaryExpr() throws ParseException { + String s = null; + Expression left = null; + if (jj_2_7(2147483647)) { + jj_consume_token(34); + left = unaryExpr(); + } else { + switch ((jjNtk == -1) ? jj_ntk() : jjNtk) { + case 35: + jj_consume_token(35); + left = unaryExpr(); + left = UnaryExpression.createNegate(left); + break; + case NOT: + jj_consume_token(NOT); + left = unaryExpr(); + left = UnaryExpression.createNOT(asBooleanExpression(left)); + break; + case TRUE: + case FALSE: + case NULL: + case DECIMAL_LITERAL: + case FLOATING_POINT_LITERAL: + case STRING_LITERAL: + case ID: + case 31: + left = primaryExpr(); + break; + default: + jjLa1[13] = jjGen; + jj_consume_token(-1); + throw new ParseException(); + } + } + return left; + } + + final public Expression primaryExpr() throws ParseException { + Expression left = null; + switch ((jjNtk == -1) ? jj_ntk() : jjNtk) { + case TRUE: + case FALSE: + case NULL: + case DECIMAL_LITERAL: + case FLOATING_POINT_LITERAL: + case STRING_LITERAL: + left = literal(); + break; + case ID: + left = variable(); + break; + case 31: + jj_consume_token(31); + left = orExpression(); + jj_consume_token(33); + break; + default: + jjLa1[14] = jjGen; + jj_consume_token(-1); + throw new ParseException(); + } + return left; + } + + final public ConstantExpression literal() throws ParseException { + Token t; + String s; + ConstantExpression left = null; + switch ((jjNtk == -1) ? jj_ntk() : jjNtk) { + case STRING_LITERAL: + s = stringLitteral(); + left = new ConstantExpression(s); + break; + case DECIMAL_LITERAL: + t = jj_consume_token(DECIMAL_LITERAL); + left = ConstantExpression.createFromDecimal(t.image); + break; + case FLOATING_POINT_LITERAL: + t = jj_consume_token(FLOATING_POINT_LITERAL); + left = ConstantExpression.createFloat(t.image); + break; + case TRUE: + jj_consume_token(TRUE); + left = BooleanConstantExpression.TRUE; + break; + case FALSE: + jj_consume_token(FALSE); + left = BooleanConstantExpression.FALSE; + break; + case NULL: + jj_consume_token(NULL); + left = BooleanConstantExpression.NULL; + break; + default: + jjLa1[15] = jjGen; + jj_consume_token(-1); + throw new ParseException(); + } + return left; + } + + final public String stringLitteral() throws ParseException { + Token t; + StringBuffer rc = new StringBuffer(); + boolean first = true; + t = jj_consume_token(STRING_LITERAL); + // Decode the sting value. + String image = t.image; + for (int i = 1; i < image.length() - 1; i++) { + char c = image.charAt(i); + if (c == '\u005c'') + i++; + rc.append(c); + } + return rc.toString(); + } + + final public PropertyExpression variable() throws ParseException { + Token t; + PropertyExpression left = null; + t = jj_consume_token(ID); + left = new PropertyExpression(t.image); + return left; + } + + private boolean jj_2_1(int xla) { + jjLa = xla; + jjLastpos = jjScanpos = token; + try { + return !jj_3_1(); + } catch (LookaheadSuccess ls) { + return true; + } finally { + jj_save(0, xla); + } + } + + private boolean jj_2_2(int xla) { + jjLa = xla; + jjLastpos = jjScanpos = token; + try { + return !jj_3_2(); + } catch (LookaheadSuccess ls) { + return true; + } finally { + jj_save(1, xla); + } + } + + private boolean jj_2_3(int xla) { + jjLa = xla; + jjLastpos = jjScanpos = token; + try { + return !jj_3_3(); + } catch (LookaheadSuccess ls) { + return true; + } finally { + jj_save(2, xla); + } + } + + private boolean jj_2_4(int xla) { + jjLa = xla; + jjLastpos = jjScanpos = token; + try { + return !jj_3_4(); + } catch (LookaheadSuccess ls) { + return true; + } finally { + jj_save(3, xla); + } + } + + private boolean jj_2_5(int xla) { + jjLa = xla; + jjLastpos = jjScanpos = token; + try { + return !jj_3_5(); + } catch (LookaheadSuccess ls) { + return true; + } finally { + jj_save(4, xla); + } + } + + private boolean jj_2_6(int xla) { + jjLa = xla; + jjLastpos = jjScanpos = token; + try { + return !jj_3_6(); + } catch (LookaheadSuccess ls) { + return true; + } finally { + jj_save(5, xla); + } + } + + private boolean jj_2_7(int xla) { + jjLa = xla; + jjLastpos = jjScanpos = token; + try { + return !jj_3_7(); + } catch (LookaheadSuccess ls) { + return true; + } finally { + jj_save(6, xla); + } + } + + private boolean jj_3R_34() { + if (jj_scan_token(26)) return true; + if (jj_3R_30()) return true; + return false; + } + + 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; + } + + private boolean jj_3R_31() { + Token xsp; + xsp = jjScanpos; + if (jj_3R_33()) { + jjScanpos = xsp; + if (jj_3R_34()) { + jjScanpos = xsp; + if (jj_3_1()) { + jjScanpos = xsp; + if (jj_3R_35()) return true; + } + } + } + return false; + } + + private boolean jj_3R_33() { + if (jj_scan_token(25)) return true; + if (jj_3R_30()) return true; + return false; + } + + 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_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_14() { + if (jj_3R_17()) return true; + return false; + } + + 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; + xsp = jjScanpos; + if (jj_3R_13()) { + jjScanpos = xsp; + if (jj_3R_14()) { + jjScanpos = xsp; + if (jj_3R_15()) return true; + } + } + return false; + } + + private boolean jj_3R_28() { + if (jj_3R_30()) return true; + Token xsp; + while (true) { + xsp = jjScanpos; + if (jj_3R_31()) { + jjScanpos = xsp; + break; + } + } + return false; + } + + 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; + return false; + } + + 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(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(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; + Token xsp; + while (true) { + xsp = jjScanpos; + if (jj_3R_29()) { + jjScanpos = xsp; + break; + } + } + return false; + } + + private boolean jj_3R_8() { + if (jj_scan_token(34)) return true; + if (jj_3R_7()) return true; + return false; + } + + private boolean jj_3R_39() { + if (jj_scan_token(30)) return true; + if (jj_3R_7()) return true; + return false; + } + + 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(29)) return true; + if (jj_3R_7()) return true; + return false; + } + + 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; + return false; + } + + private boolean jj_3R_37() { + if (jj_scan_token(28)) return true; + if (jj_3R_7()) return true; + return false; + } + + private boolean jj_3R_24() { + if (jj_scan_token(NULL)) return true; + return false; + } + + private boolean jj_3R_36() { + if (jj_scan_token(27)) return true; + if (jj_3R_7()) return true; + return false; + } + + private boolean jj_3R_32() { + Token xsp; + xsp = jjScanpos; + if (jj_3R_36()) { + jjScanpos = xsp; + if (jj_3R_37()) { + jjScanpos = xsp; + if (jj_3R_38()) { + jjScanpos = xsp; + if (jj_3R_39()) { + jjScanpos = xsp; + if (jj_3R_40()) { + jjScanpos = xsp; + if (jj_3_2()) { + jjScanpos = xsp; + if (jj_3R_41()) { + jjScanpos = xsp; + 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; + } + } + } + } + } + } + } + } + } + } + } + } + } + return false; + } + + private boolean jj_3R_23() { + if (jj_scan_token(FALSE)) return true; + return false; + } + + 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_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_46()) { + jjScanpos = xsp; + break; + } + } + if (jj_scan_token(33)) return true; + return false; + } + + private boolean jj_3R_45() { + if (jj_scan_token(32)) return true; + if (jj_3R_27()) return true; + return false; + } + + 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_21() { + if (jj_scan_token(FLOATING_POINT_LITERAL)) return true; + return false; + } + + private boolean jj_3R_20() { + if (jj_scan_token(DECIMAL_LITERAL)) 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)) 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_45()) { + jjScanpos = xsp; + break; + } + } + if (jj_scan_token(33)) return true; + return false; + } + + 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; + } + + /** + * Generated Token Manager. + */ + public SelectorParserTokenManager tokenSource; + SimpleCharStream jjInputStream; + /** + * Current token. + */ + public Token token; + /** + * Next token. + */ + public Token jjNt; + private int jjNtk; + private Token jjScanpos, jjLastpos; + private int jjLa; + private int jjGen; + final private int[] jjLa1 = new int[16]; + static private int[] jjLa10; + static private int[] jjLa11; + + static { + jj_la1_init_0(); + jj_la1_init_1(); + } + + private static void jj_la1_init_0() { + 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, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0,}; + } + + final private JJCalls[] jj2Rtns = new JJCalls[7]; + private boolean jjRescan = false; + private int jjGc = 0; + + /** + * Constructor with InputStream. + */ + public SelectorParser(java.io.InputStream stream) { + this(stream, null); + } + + /** + * Constructor with InputStream and supplied encoding + */ + public SelectorParser(java.io.InputStream stream, String encoding) { + try { + jjInputStream = new SimpleCharStream(stream, encoding, 1, 1); + } catch (java.io.UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + tokenSource = new SelectorParserTokenManager(jjInputStream); + token = new Token(); + jjNtk = -1; + jjGen = 0; + for (int i = 0; i < 16; i++) jjLa1[i] = -1; + for (int i = 0; i < jj2Rtns.length; i++) jj2Rtns[i] = new JJCalls(); + } + + /** + * Reinitialise. + */ + public void ReInit(java.io.InputStream stream) { + ReInit(stream, null); + } + + /** + * Reinitialise. + */ + public void ReInit(java.io.InputStream stream, String encoding) { + try { + jjInputStream.ReInit(stream, encoding, 1, 1); + } catch (java.io.UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + tokenSource.ReInit(jjInputStream); + token = new Token(); + jjNtk = -1; + jjGen = 0; + for (int i = 0; i < 16; i++) jjLa1[i] = -1; + for (int i = 0; i < jj2Rtns.length; i++) jj2Rtns[i] = new JJCalls(); + } + + /** + * Constructor. + */ + public SelectorParser(java.io.Reader stream) { + jjInputStream = new SimpleCharStream(stream, 1, 1); + tokenSource = new SelectorParserTokenManager(jjInputStream); + token = new Token(); + jjNtk = -1; + jjGen = 0; + for (int i = 0; i < 16; i++) jjLa1[i] = -1; + for (int i = 0; i < jj2Rtns.length; i++) jj2Rtns[i] = new JJCalls(); + } + + /** + * Reinitialise. + */ + public void ReInit(java.io.Reader stream) { + jjInputStream.ReInit(stream, 1, 1); + tokenSource.ReInit(jjInputStream); + token = new Token(); + jjNtk = -1; + jjGen = 0; + for (int i = 0; i < 16; i++) jjLa1[i] = -1; + for (int i = 0; i < jj2Rtns.length; i++) jj2Rtns[i] = new JJCalls(); + } + + /** + * Constructor with generated Token Manager. + */ + public SelectorParser(SelectorParserTokenManager tm) { + tokenSource = tm; + token = new Token(); + jjNtk = -1; + jjGen = 0; + for (int i = 0; i < 16; i++) jjLa1[i] = -1; + for (int i = 0; i < jj2Rtns.length; i++) jj2Rtns[i] = new JJCalls(); + } + + /** + * Reinitialise. + */ + public void ReInit(SelectorParserTokenManager tm) { + tokenSource = tm; + token = new Token(); + jjNtk = -1; + jjGen = 0; + 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(); + jjNtk = -1; + if (token.kind == kind) { + jjGen++; + if (++jjGc > 100) { + jjGc = 0; + for (int i = 0; i < jj2Rtns.length; i++) { + JJCalls c = jj2Rtns[i]; + while (c != null) { + if (c.gen < jjGen) c.first = null; + c = c.next; + } + } + } + return token; + } + token = oldToken; + jjKind = kind; + throw generateParseException(); + } + + static private final class LookaheadSuccess extends java.lang.Error { + } + + final private LookaheadSuccess jjLs = new LookaheadSuccess(); + + private boolean jj_scan_token(int kind) { + if (jjScanpos == jjLastpos) { + jjLa--; + if (jjScanpos.next == null) { + jjLastpos = jjScanpos = jjScanpos.next = tokenSource.getNextToken(); + } else { + jjLastpos = jjScanpos = jjScanpos.next; + } + } else { + jjScanpos = jjScanpos.next; + } + if (jjRescan) { + int i = 0; + Token tok = token; + while (tok != null && tok != jjScanpos) { + i++; + tok = tok.next; + } + if (tok != null) jj_add_error_token(kind, i); + } + 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(); + jjNtk = -1; + jjGen++; + return token; + } + + /** + * Get the specific Token. + */ + 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(); + } + return t; + } + + private int jj_ntk() { + if ((jjNt = token.next) == null) + return jjNtk = (token.next = tokenSource.getNextToken()).kind; + else + return jjNtk = jjNt.kind; + } + + 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 == jjEndpos + 1) { + jjLasttokens[jjEndpos++] = kind; + } else if (jjEndpos != 0) { + jjExpentry = new int[jjEndpos]; + for (int i = 0; i < jjEndpos; i++) { + jjExpentry[i] = jjLasttokens[i]; + } + 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]) { + exists = false; + break; + } + } + if (exists) break; + } + } + if (!exists) jjExpentries.add(jjExpentry); + if (pos != 0) jjLasttokens[(jjEndpos = pos) - 1] = kind; + } + } + + /** + * Generate ParseException. + */ + public ParseException generateParseException() { + jjExpentries.clear(); + boolean[] la1tokens = new boolean[36]; + if (jjKind >= 0) { + la1tokens[jjKind] = true; + jjKind = -1; + } + for (int i = 0; i < 16; i++) { + if (jjLa1[i] == jjGen) { + for (int j = 0; j < 32; j++) { + if ((jjLa10[i] & (1 << j)) != 0) { + la1tokens[j] = true; + } + if ((jjLa11[i] & (1 << j)) != 0) { + la1tokens[32 + j] = true; + } + } + } + } + for (int i = 0; i < 36; i++) { + if (la1tokens[i]) { + jjExpentry = new int[1]; + jjExpentry[0] = i; + jjExpentries.add(jjExpentry); + } + } + jjEndpos = 0; + jj_rescan_token(); + jj_add_error_token(0, 0); + int[][] exptokseq = new int[jjExpentries.size()][]; + for (int i = 0; i < jjExpentries.size(); i++) { + exptokseq[i] = jjExpentries.get(i); + } + return new ParseException(token, exptokseq, TOKEN_IMAGE); + } + + /** + * Enable tracing. + */ + final public void enable_tracing() { + } + + /** + * Disable tracing. + */ + final public void disable_tracing() { + } + + private void jj_rescan_token() { + jjRescan = true; + for (int i = 0; i < 7; i++) { + try { + JJCalls p = jj2Rtns[i]; + do { + if (p.gen > jjGen) { + jjLa = p.arg; + jjLastpos = jjScanpos = p.first; + switch (i) { + case 0: + jj_3_1(); + break; + case 1: + jj_3_2(); + break; + case 2: + jj_3_3(); + break; + 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); + } catch (LookaheadSuccess ls) { + } + } + jjRescan = false; + } + + private void jj_save(int index, int xla) { + JJCalls p = jj2Rtns[index]; + while (p.gen > jjGen) { + if (p.next == null) { + p = p.next = new JJCalls(); + break; + } + p = p.next; + } + p.gen = jjGen + xla - jjLa; + p.first = token; + p.arg = xla; + } + + static final class JJCalls { + int gen; + Token first; + int arg; + JJCalls next; + } + +} 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 new file mode 100644 index 0000000..f263696 --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/parser/SelectorParser.jj @@ -0,0 +1,558 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * This file was taken from ActiveMQ activemq-client/src/main/grammar/SelectorParser.jj. + * + * There are some modifications: + * 1. Convert string expressions default; + * 2. HEX_LITERAL and OCTAL_LITERAL were removed; + * 3. LIKE, ESCAPE, XPATH and XQUERY were removed; + * 4. Computation expressions were removed; + */ + +// ---------------------------------------------------------------------------- +// OPTIONS +// ---------------------------------------------------------------------------- +options { + STATIC = false; + UNICODE_INPUT = true; + + //ERROR_REPORTING = false; +} + +// ---------------------------------------------------------------------------- +// PARSER +// ---------------------------------------------------------------------------- + +PARSER_BEGIN(SelectorParser) + +package org.apache.rocketmq.filter.parser; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import org.apache.rocketmq.filter.expression.BooleanExpression; +import org.apache.rocketmq.filter.expression.ComparisonExpression; +import org.apache.rocketmq.filter.expression.ConstantExpression; +import org.apache.rocketmq.filter.expression.Expression; +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 java.io.StringReader; +import java.util.ArrayList; + +/** + * JMS Selector Parser generated by JavaCC + * + * Do not edit this .java file directly - it is autogenerated from SelectorParser.jj + */ +public class SelectorParser { + + private static final Cache PARSE_CACHE = CacheBuilder.newBuilder().maximumSize(100).build(); +// private static final String CONVERT_STRING_EXPRESSIONS_PREFIX = "convert_string_expressions:"; + + public static BooleanExpression parse(String sql) throws MQFilterException { +// sql = "("+sql+")"; + Object result = PARSE_CACHE.getIfPresent(sql); + if (result instanceof MQFilterException) { + throw (MQFilterException) result; + } else if (result instanceof BooleanExpression) { + 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); +// } + ComparisonExpression.CONVERT_STRING_EXPRESSIONS.set(true); + try { + + BooleanExpression e = new SelectorParser(sql).parse(); + PARSE_CACHE.put(sql, e); + return e; + } catch (MQFilterException t) { + PARSE_CACHE.put(sql, t); + throw t; + } finally { + ComparisonExpression.CONVERT_STRING_EXPRESSIONS.remove(); +// if( convertStringExpressions ) { +// ComparisonExpression.CONVERT_STRING_EXPRESSIONS.remove(); +// } + } + } + } + + public static void clearCache() { + PARSE_CACHE.cleanUp(); + } + + private String sql; + + protected SelectorParser(String sql) { + this(new StringReader(sql)); + this.sql = sql; + } + + protected BooleanExpression parse() throws MQFilterException { + try { + return this.JmsSelector(); + } + catch (Throwable e) { + throw new MQFilterException("Invalid MessageSelector. ", e); + } + } + + private BooleanExpression asBooleanExpression(Expression value) throws ParseException { + if (value instanceof BooleanExpression) { + return (BooleanExpression) value; + } + if (value instanceof PropertyExpression) { + return UnaryExpression.createBooleanCast( value ); + } + throw new ParseException("Expression will not result in a boolean value: " + value); + } + +} + +PARSER_END(SelectorParser) + +// ---------------------------------------------------------------------------- +// Tokens +// ---------------------------------------------------------------------------- + +/* White Space */ +SPECIAL_TOKEN : +{ + " " | "\t" | "\n" | "\r" | "\f" +} + +/* Comments */ +SKIP: +{ + +} + +SKIP: +{ + +} + +/* Reserved Words */ +TOKEN [IGNORE_CASE] : +{ + < NOT : "NOT"> + | < AND : "AND"> + | < OR : "OR"> + | < BETWEEN : "BETWEEN"> + | < IN : "IN"> + | < TRUE : "TRUE" > + | < FALSE : "FALSE" > + | < NULL : "NULL" > + | < IS : "IS" > + | < CONTAINS : "CONTAINS"> + | < STARTSWITH : "STARTSWITH"> + | < ENDSWITH : "ENDSWITH"> +} + +/* Literals */ +TOKEN [IGNORE_CASE] : + +{ + + < DECIMAL_LITERAL: "0" | ["1"-"9"] (["0"-"9"])* (["l","L"])? > + | < FLOATING_POINT_LITERAL: + (["0"-"9"])+ "." (["0"-"9"])* ()? // matches: 5.5 or 5. or 5.5E10 or 5.E10 + | "." (["0"-"9"])+ ()? // matches: .5 or .5E10 + | (["0"-"9"])+ // matches: 5E10 + > + | < #EXPONENT: "E" (["+","-"])? (["0"-"9"])+ > + | < STRING_LITERAL: "'" ( ("''") | ~["'"] )* "'" > +} + +TOKEN [IGNORE_CASE] : +{ + < ID : ["a"-"z", "_", "$"] (["a"-"z","0"-"9","_", "$"])* > +} + +// ---------------------------------------------------------------------------- +// Grammar +// ---------------------------------------------------------------------------- +BooleanExpression JmsSelector() : +{ + Expression left=null; +} +{ + ( + left = orExpression() + ) + { + return asBooleanExpression(left); + } + +} + +Expression orExpression() : +{ + Expression left; + Expression right; +} +{ + ( + left = andExpression() + ( + right = andExpression() + { + left = LogicExpression.createOR(asBooleanExpression(left), asBooleanExpression(right)); + } + )* + ) + { + return left; + } + +} + + +Expression andExpression() : +{ + Expression left; + Expression right; +} +{ + ( + left = equalityExpression() + ( + right = equalityExpression() + { + left = LogicExpression.createAND(asBooleanExpression(left), asBooleanExpression(right)); + } + )* + ) + { + return left; + } +} + +Expression equalityExpression() : +{ + Expression left; + Expression right; +} +{ + ( + left = comparisonExpression() + ( + + "=" right = comparisonExpression() + { + left = ComparisonExpression.createEqual(left, right); + } + | + "<>" right = comparisonExpression() + { + left = ComparisonExpression.createNotEqual(left, right); + } + | + LOOKAHEAD(2) + + { + left = ComparisonExpression.createIsNull(left); + } + | + + { + left = ComparisonExpression.createIsNotNull(left); + } + )* + ) + { + return left; + } +} + +Expression comparisonExpression() : +{ + Expression left; + Expression right; + Expression low; + Expression high; + String t, u; + boolean not; + ArrayList list; +} +{ + ( + left = unaryExpr() + ( + + ">" right = unaryExpr() + { + left = ComparisonExpression.createGreaterThan(left, right); + } + | + ">=" right = unaryExpr() + { + left = ComparisonExpression.createGreaterThanEqual(left, right); + } + | + "<" right = unaryExpr() + { + left = ComparisonExpression.createLessThan(left, right); + } + | + "<=" right = unaryExpr() + { + 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() + { + left = ComparisonExpression.createBetween(left, low, high); + } + | + LOOKAHEAD(2) + low = unaryExpr() high = unaryExpr() + { + left = ComparisonExpression.createNotBetween(left, low, high); + } + | + + "(" + t = stringLitteral() + { + list = new ArrayList(); + list.add( t ); + } + ( + "," + t = stringLitteral() + { + list.add( t ); + } + + )* + ")" + { + left = ComparisonExpression.createInFilter(left, list); + } + | + LOOKAHEAD(2) + + "(" + t = stringLitteral() + { + list = new ArrayList(); + list.add( t ); + } + ( + "," + t = stringLitteral() + { + list.add( t ); + } + + )* + ")" + { + left = ComparisonExpression.createNotInFilter(left, list); + } + + )* + ) + { + return left; + } +} + +Expression unaryExpr() : +{ + String s=null; + Expression left=null; +} +{ + ( + LOOKAHEAD( "+" unaryExpr() ) + "+" left=unaryExpr() + | + "-" left=unaryExpr() + { + left = UnaryExpression.createNegate(left); + } + | + left=unaryExpr() + { + left = UnaryExpression.createNOT( asBooleanExpression(left) ); + } + | + left = primaryExpr() + ) + { + return left; + } + +} + +Expression primaryExpr() : +{ + Expression left=null; +} +{ + ( + left = literal() + | + left = variable() + | + "(" left = orExpression() ")" + ) + { + return left; + } +} + + + +ConstantExpression literal() : +{ + Token t; + String s; + ConstantExpression left=null; +} +{ + ( + ( + s = stringLitteral() + { + left = new ConstantExpression(s); + } + ) + | + ( + t = + { + left = ConstantExpression.createFromDecimal(t.image); + } + ) + | + ( + t = + { + left = ConstantExpression.createFloat(t.image); + } + ) + | + ( + + { + left = ConstantExpression.TRUE; + } + ) + | + ( + + { + left = ConstantExpression.FALSE; + } + ) + | + ( + + { + left = ConstantExpression.NULL; + } + ) + ) + { + return left; + } +} + +String stringLitteral() : +{ + Token t; + StringBuffer rc = new StringBuffer(); + boolean first=true; +} +{ + t = + { + // Decode the sting value. + String image = t.image; + for( int i=1; i < image.length()-1; i++ ) { + char c = image.charAt(i); + if( c == '\'' ) + i++; + rc.append(c); + } + return rc.toString(); + } +} + +PropertyExpression variable() : +{ + Token t; + PropertyExpression left=null; +} +{ + ( + t = + { + left = new PropertyExpression(t.image); + } + ) + { + return left; + } +} 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 new file mode 100644 index 0000000..8f228be --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/parser/SelectorParserConstants.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. + */ + +/* Generated By:JavaCC: Do not edit this line. SelectorParserConstants.java */ +package org.apache.rocketmq.filter.parser; + +/** + * Token literal values and constants. + * Generated by org.javacc.parser.OtherFilesGen#start() + */ +public interface SelectorParserConstants { + + /** + * End of File. + */ + int EOF = 0; + /** + * RegularExpression Id. + */ + int LINE_COMMENT = 6; + /** + * RegularExpression Id. + */ + int BLOCK_COMMENT = 7; + /** + * RegularExpression Id. + */ + int NOT = 8; + /** + * RegularExpression Id. + */ + int AND = 9; + /** + * RegularExpression Id. + */ + int OR = 10; + /** + * RegularExpression Id. + */ + int BETWEEN = 11; + /** + * RegularExpression Id. + */ + int IN = 12; + /** + * RegularExpression Id. + */ + int TRUE = 13; + /** + * RegularExpression Id. + */ + int FALSE = 14; + /** + * RegularExpression Id. + */ + int NULL = 15; + /** + * RegularExpression Id. + */ + int IS = 16; + /** + * RegularExpression Id. + */ + int CONTAINS = 17; + /** + * RegularExpression Id. + */ + int STARTSWITH = 18; + /** + * RegularExpression Id. + */ + int ENDSWITH = 19; + /** + * RegularExpression Id. + */ + int DECIMAL_LITERAL = 20; + /** + * RegularExpression Id. + */ + int FLOATING_POINT_LITERAL = 21; + /** + * RegularExpression Id. + */ + int EXPONENT = 22; + /** + * RegularExpression Id. + */ + int STRING_LITERAL = 23; + /** + * RegularExpression Id. + */ + int ID = 24; + + /** + * Lexical state. + */ + int DEFAULT = 0; + + /** + * Literal token values. + */ + String[] TOKEN_IMAGE = { + "", + "\" \"", + "\"\\t\"", + "\"\\n\"", + "\"\\r\"", + "\"\\f\"", + "", + "", + "\"NOT\"", + "\"AND\"", + "\"OR\"", + "\"BETWEEN\"", + "\"IN\"", + "\"TRUE\"", + "\"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 new file mode 100644 index 0000000..6d9b855 --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/parser/SelectorParserTokenManager.java @@ -0,0 +1,1066 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Generated By:JavaCC: Do not edit this line. SelectorParserTokenManager.java */ +package org.apache.rocketmq.filter.parser; + +/** + * Token Manager. + */ +public class SelectorParserTokenManager implements SelectorParserConstants { + + /** + * Debug output. + */ + public java.io.PrintStream debugStream = System.out; + + /** + * Set debug output. + */ + public void setDebugStream(java.io.PrintStream ds) { + debugStream = ds; + } + + private int jjStopAtPos(int pos, int kind) { + jjmatchedKind = kind; + jjmatchedPos = pos; + return pos + 1; + } + + private int jjMoveStringLiteralDfa0_0() { + switch (curChar) { + case 9: + jjmatchedKind = 2; + return jjMoveNfa_0(5, 0); + case 10: + jjmatchedKind = 3; + return jjMoveNfa_0(5, 0); + case 12: + jjmatchedKind = 5; + return jjMoveNfa_0(5, 0); + case 13: + jjmatchedKind = 4; + return jjMoveNfa_0(5, 0); + case 32: + jjmatchedKind = 1; + return jjMoveNfa_0(5, 0); + case 40: + jjmatchedKind = 31; + return jjMoveNfa_0(5, 0); + case 41: + jjmatchedKind = 33; + return jjMoveNfa_0(5, 0); + case 43: + jjmatchedKind = 34; + return jjMoveNfa_0(5, 0); + case 44: + jjmatchedKind = 32; + return jjMoveNfa_0(5, 0); + case 45: + jjmatchedKind = 35; + return jjMoveNfa_0(5, 0); + case 60: + jjmatchedKind = 29; + return jjMoveStringLiteralDfa1_0(0x44000000L); + case 61: + jjmatchedKind = 25; + return jjMoveNfa_0(5, 0); + case 62: + 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: + return jjMoveStringLiteralDfa1_0(0x11000L); + case 78: + 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: + return jjMoveStringLiteralDfa1_0(0x11000L); + case 110: + return jjMoveStringLiteralDfa1_0(0x8100L); + case 111: + return jjMoveStringLiteralDfa1_0(0x400L); + case 115: + return jjMoveStringLiteralDfa1_0(0x40000L); + case 116: + return jjMoveStringLiteralDfa1_0(0x2000L); + default: + return jjMoveNfa_0(5, 0); + } + } + + private int jjMoveStringLiteralDfa1_0(long active0) { + try { + curChar = inputStream.readChar(); + } catch (java.io.IOException e) { + return jjMoveNfa_0(5, 0); + } + switch (curChar) { + case 61: + if ((active0 & 0x10000000L) != 0L) { + jjmatchedKind = 28; + jjmatchedPos = 1; + } else if ((active0 & 0x40000000L) != 0L) { + jjmatchedKind = 30; + jjmatchedPos = 1; + } + break; + case 62: + if ((active0 & 0x4000000L) != 0L) { + jjmatchedKind = 26; + jjmatchedPos = 1; + } + break; + case 65: + return jjMoveStringLiteralDfa2_0(active0, 0x4000L); + case 69: + return jjMoveStringLiteralDfa2_0(active0, 0x800L); + case 78: + if ((active0 & 0x1000L) != 0L) { + jjmatchedKind = 12; + jjmatchedPos = 1; + } + return jjMoveStringLiteralDfa2_0(active0, 0x80200L); + case 79: + return jjMoveStringLiteralDfa2_0(active0, 0x20100L); + case 82: + if ((active0 & 0x400L) != 0L) { + jjmatchedKind = 10; + jjmatchedPos = 1; + } + return jjMoveStringLiteralDfa2_0(active0, 0x2000L); + case 83: + if ((active0 & 0x10000L) != 0L) { + jjmatchedKind = 16; + jjmatchedPos = 1; + } + break; + case 84: + return jjMoveStringLiteralDfa2_0(active0, 0x40000L); + case 85: + return jjMoveStringLiteralDfa2_0(active0, 0x8000L); + case 97: + return jjMoveStringLiteralDfa2_0(active0, 0x4000L); + case 101: + return jjMoveStringLiteralDfa2_0(active0, 0x800L); + case 110: + if ((active0 & 0x1000L) != 0L) { + jjmatchedKind = 12; + jjmatchedPos = 1; + } + return jjMoveStringLiteralDfa2_0(active0, 0x80200L); + case 111: + return jjMoveStringLiteralDfa2_0(active0, 0x20100L); + case 114: + if ((active0 & 0x400L) != 0L) { + jjmatchedKind = 10; + jjmatchedPos = 1; + } + return jjMoveStringLiteralDfa2_0(active0, 0x2000L); + case 115: + if ((active0 & 0x10000L) != 0L) { + jjmatchedKind = 16; + jjmatchedPos = 1; + } + break; + case 116: + return jjMoveStringLiteralDfa2_0(active0, 0x40000L); + case 117: + return jjMoveStringLiteralDfa2_0(active0, 0x8000L); + default: + break; + } + return jjMoveNfa_0(5, 1); + } + + private int jjMoveStringLiteralDfa2_0(long old0, long active0) { + if (((active0 &= old0)) == 0L) + return jjMoveNfa_0(5, 1); + try { + curChar = inputStream.readChar(); + } catch (java.io.IOException e) { + return jjMoveNfa_0(5, 1); + } + switch (curChar) { + case 65: + return jjMoveStringLiteralDfa3_0(active0, 0x40000L); + case 68: + if ((active0 & 0x200L) != 0L) { + jjmatchedKind = 9; + jjmatchedPos = 2; + } + 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; + jjmatchedPos = 2; + } + 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; + } + 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; + jjmatchedPos = 2; + } + return jjMoveStringLiteralDfa3_0(active0, 0x800L); + case 117: + return jjMoveStringLiteralDfa3_0(active0, 0x2000L); + default: + break; + } + return jjMoveNfa_0(5, 2); + } + + private int jjMoveStringLiteralDfa3_0(long old0, long active0) { + if (((active0 &= old0)) == 0L) + return jjMoveNfa_0(5, 2); + try { + curChar = inputStream.readChar(); + } catch (java.io.IOException e) { + return jjMoveNfa_0(5, 2); + } + switch (curChar) { + case 69: + if ((active0 & 0x2000L) != 0L) { + jjmatchedKind = 13; + jjmatchedPos = 3; + } + break; + case 76: + if ((active0 & 0x8000L) != 0L) { + jjmatchedKind = 15; + jjmatchedPos = 3; + } + break; + case 82: + return jjMoveStringLiteralDfa4_0(active0, 0x40000L); + case 83: + return jjMoveStringLiteralDfa4_0(active0, 0x84000L); + case 84: + return jjMoveStringLiteralDfa4_0(active0, 0x20000L); + case 87: + return jjMoveStringLiteralDfa4_0(active0, 0x800L); + case 101: + if ((active0 & 0x2000L) != 0L) { + jjmatchedKind = 13; + jjmatchedPos = 3; + } + break; + case 108: + if ((active0 & 0x8000L) != 0L) { + jjmatchedKind = 15; + jjmatchedPos = 3; + } + break; + case 114: + return jjMoveStringLiteralDfa4_0(active0, 0x40000L); + case 115: + return jjMoveStringLiteralDfa4_0(active0, 0x84000L); + case 116: + return jjMoveStringLiteralDfa4_0(active0, 0x20000L); + case 119: + return jjMoveStringLiteralDfa4_0(active0, 0x800L); + default: + break; + } + return jjMoveNfa_0(5, 3); + } + + private int jjMoveStringLiteralDfa4_0(long old0, long active0) { + if (((active0 &= old0)) == 0L) + return jjMoveNfa_0(5, 3); + try { + curChar = inputStream.readChar(); + } catch (java.io.IOException e) { + 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; + } + return jjMoveNfa_0(5, 4); + } + + private int jjMoveStringLiteralDfa5_0(long old0, long active0) { + if (((active0 &= old0)) == 0L) + return jjMoveNfa_0(5, 4); + try { + curChar = inputStream.readChar(); + } catch (java.io.IOException e) { + return jjMoveNfa_0(5, 4); + } + 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; + } + return jjMoveNfa_0(5, 5); + } + + private int jjMoveStringLiteralDfa6_0(long old0, long active0) { + if (((active0 &= old0)) == 0L) + return jjMoveNfa_0(5, 5); + try { + curChar = inputStream.readChar(); + } catch (java.io.IOException e) { + return jjMoveNfa_0(5, 5); + } + switch (curChar) { + case 78: + if ((active0 & 0x800L) != 0L) { + jjmatchedKind = 11; + jjmatchedPos = 6; + } + 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; + } + 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 + }; + static final long[] JJ_BIT_VEC_2 = { + 0x0L, 0x0L, 0xffffffffffffffffL, 0xffffffffffffffffL + }; + + private int jjMoveNfa_0(int startState, int curPos) { + int strKind = jjmatchedKind; + int strPos = jjmatchedPos; + int seenUpto; + inputStream.backup(seenUpto = curPos + 1); + try { + curChar = inputStream.readChar(); + } catch (java.io.IOException e) { + throw new Error("Internal Error"); + } + curPos = 0; + int startsAt = 0; + jjnewStateCnt = 40; + int i = 1; + jjstateSet[0] = startState; + int kind = 0x7fffffff; + for (; ; ) { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) { + long l = 1L << curChar; + do { + switch (jjstateSet[--i]) { + case 5: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddStates(0, 3); + else if (curChar == 36) { + if (kind > 24) + kind = 24; + jjCheckNAdd(28); + } else if (curChar == 39) + jjCheckNAddStates(4, 6); + else if (curChar == 46) + jjCheckNAdd(18); + else if (curChar == 47) + jjstateSet[jjnewStateCnt++] = 6; + else if (curChar == 45) + jjstateSet[jjnewStateCnt++] = 0; + if ((0x3fe000000000000L & l) != 0L) { + if (kind > 20) + kind = 20; + jjCheckNAddTwoStates(15, 16); + } else if (curChar == 48) { + if (kind > 20) + kind = 20; + } + break; + case 0: + if (curChar == 45) + jjCheckNAddStates(7, 9); + break; + case 1: + if ((0xffffffffffffdbffL & l) != 0L) + jjCheckNAddStates(7, 9); + break; + case 2: + if ((0x2400L & l) != 0L && kind > 6) + kind = 6; + break; + case 3: + if (curChar == 10 && kind > 6) + kind = 6; + break; + case 4: + if (curChar == 13) + jjstateSet[jjnewStateCnt++] = 3; + break; + case 6: + if (curChar == 42) + jjCheckNAddTwoStates(7, 8); + break; + case 7: + if ((0xfffffbffffffffffL & l) != 0L) + jjCheckNAddTwoStates(7, 8); + break; + case 8: + if (curChar == 42) + jjCheckNAddStates(10, 12); + break; + case 9: + if ((0xffff7bffffffffffL & l) != 0L) + jjCheckNAddTwoStates(10, 8); + break; + case 10: + if ((0xfffffbffffffffffL & l) != 0L) + jjCheckNAddTwoStates(10, 8); + break; + case 11: + if (curChar == 47 && kind > 7) + kind = 7; + break; + case 12: + if (curChar == 47) + jjstateSet[jjnewStateCnt++] = 6; + break; + case 13: + if (curChar == 48 && kind > 20) + kind = 20; + break; + case 14: + if ((0x3fe000000000000L & l) == 0L) + break; + if (kind > 20) + kind = 20; + jjCheckNAddTwoStates(15, 16); + break; + case 15: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 20) + kind = 20; + jjCheckNAddTwoStates(15, 16); + break; + case 17: + if (curChar == 46) + jjCheckNAdd(18); + break; + case 18: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 21) + kind = 21; + jjCheckNAddTwoStates(18, 19); + break; + case 20: + if ((0x280000000000L & l) != 0L) + jjCheckNAdd(21); + break; + case 21: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 21) + kind = 21; + jjCheckNAdd(21); + break; + case 22: + case 23: + if (curChar == 39) + jjCheckNAddStates(4, 6); + break; + case 24: + if (curChar == 39) + jjstateSet[jjnewStateCnt++] = 23; + break; + case 25: + if ((0xffffff7fffffffffL & l) != 0L) + jjCheckNAddStates(4, 6); + break; + case 26: + if (curChar == 39 && kind > 23) + kind = 23; + break; + case 27: + if (curChar != 36) + break; + if (kind > 24) + kind = 24; + jjCheckNAdd(28); + break; + case 28: + if ((0x3ff001000000000L & l) == 0L) + break; + if (kind > 24) + kind = 24; + jjCheckNAdd(28); + break; + case 29: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddStates(0, 3); + break; + case 30: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(30, 31); + break; + case 31: + if (curChar != 46) + break; + if (kind > 21) + kind = 21; + jjCheckNAddTwoStates(32, 33); + break; + case 32: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 21) + kind = 21; + jjCheckNAddTwoStates(32, 33); + break; + case 34: + if ((0x280000000000L & l) != 0L) + jjCheckNAdd(35); + break; + case 35: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 21) + kind = 21; + jjCheckNAdd(35); + break; + case 36: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(36, 37); + break; + case 38: + if ((0x280000000000L & l) != 0L) + jjCheckNAdd(39); + break; + case 39: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 21) + kind = 21; + jjCheckNAdd(39); + break; + default: + break; + } + } while (i != startsAt); + } else if (curChar < 128) { + long l = 1L << (curChar & 077); + do { + switch (jjstateSet[--i]) { + case 5: + case 28: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 24) + kind = 24; + jjCheckNAdd(28); + break; + case 1: + jjAddStates(7, 9); + break; + case 7: + jjCheckNAddTwoStates(7, 8); + break; + case 9: + case 10: + jjCheckNAddTwoStates(10, 8); + break; + case 16: + if ((0x100000001000L & l) != 0L && kind > 20) + kind = 20; + break; + case 19: + if ((0x2000000020L & l) != 0L) + jjAddStates(13, 14); + break; + case 25: + jjAddStates(4, 6); + break; + case 33: + if ((0x2000000020L & l) != 0L) + jjAddStates(15, 16); + break; + case 37: + if ((0x2000000020L & l) != 0L) + jjAddStates(17, 18); + break; + default: + break; + } + } while (i != startsAt); + } else { + int hiByte = (int) (curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + do { + switch (jjstateSet[--i]) { + case 1: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + jjAddStates(7, 9); + break; + case 7: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(7, 8); + break; + case 9: + case 10: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(10, 8); + break; + case 25: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + jjAddStates(4, 6); + break; + default: + break; + } + } while (i != startsAt); + } + if (kind != 0x7fffffff) { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 40 - (jjnewStateCnt = startsAt))) + break; + try { + curChar = inputStream.readChar(); + } catch (java.io.IOException e) { + break; + } + } + if (jjmatchedPos > strPos) + return curPos; + + int toRet = Math.max(curPos, seenUpto); + + if (curPos < toRet) + for (i = toRet - Math.min(curPos, seenUpto); i-- > 0; ) + try { + curChar = inputStream.readChar(); + } catch (java.io.IOException e) { + throw new Error("Internal Error : Please send a bug report."); + } + + if (jjmatchedPos < strPos) { + jjmatchedKind = strKind; + jjmatchedPos = strPos; + } else if (jjmatchedPos == strPos && jjmatchedKind > strKind) + jjmatchedKind = strKind; + + return toRet; + } + + static final int[] JJ_NEXT_STATES = { + 30, 31, 36, 37, 24, 25, 26, 1, 2, 4, 8, 9, 11, 20, 21, 34, + 35, 38, 39, + }; + + private static final boolean jjCanMove_0(int hiByte, int i1, int i2, long l1, long l2) { + switch (hiByte) { + case 0: + return (JJ_BIT_VEC_2[i2] & l2) != 0L; + default: + if ((JJ_BIT_VEC_0[i1] & l1) != 0L) + return true; + return false; + } + } + + /** + * Token literal values. + */ + 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, null, null, null, "\75", + "\74\76", "\76", "\76\75", "\74", "\74\75", "\50", "\54", "\51", "\53", "\55",}; + + /** + * Lexer state names. + */ + public static final String[] LEX_STATE_NAMES = { + "DEFAULT", + }; + static final long[] JJ_TO_TOKEN = { + 0xfffbfff01L, + }; + static final long[] JJ_TO_SKIP = { + 0xfeL, + }; + static final long[] JJ_TO_SPECIAL = { + 0x3eL, + }; + protected SimpleCharStream inputStream; + private final int[] jjrounds = new int[40]; + private final int[] jjstateSet = new int[80]; + protected char curChar; + + /** + * Constructor. + */ + public SelectorParserTokenManager(SimpleCharStream stream) { + if (SimpleCharStream.STATIC_FLAG) + throw new Error("ERROR: Cannot use a static CharStream class with a non-static lexical analyzer."); + inputStream = stream; + } + + /** + * Constructor. + */ + public SelectorParserTokenManager(SimpleCharStream stream, int lexState) { + this(stream); + SwitchTo(lexState); + } + + /** + * Reinitialise parser. + */ + public void ReInit(SimpleCharStream stream) { + jjmatchedPos = jjnewStateCnt = 0; + curLexState = defaultLexState; + inputStream = stream; + ReInitRounds(); + } + + private void ReInitRounds() { + int i; + jjround = 0x80000001; + for (i = 40; i-- > 0; ) + jjrounds[i] = 0x80000000; + } + + /** + * Reinitialise parser. + */ + public void ReInit(SimpleCharStream stream, int lexState) { + ReInit(stream); + SwitchTo(lexState); + } + + /** + * Switch to specified lex state. + */ + 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); + else + curLexState = lexState; + } + + protected Token jjFillToken() { + final Token t; + final String curTokenImage; + final int beginLine; + final int endLine; + final int beginColumn; + final int endColumn; + String im = JJ_STR_LITERAL_IMAGES[jjmatchedKind]; + curTokenImage = (im == null) ? inputStream.GetImage() : im; + beginLine = inputStream.getBeginLine(); + beginColumn = inputStream.getBeginColumn(); + endLine = inputStream.getEndLine(); + endColumn = inputStream.getEndColumn(); + t = Token.newToken(jjmatchedKind, curTokenImage); + + t.beginLine = beginLine; + t.endLine = endLine; + t.beginColumn = beginColumn; + t.endColumn = endColumn; + + return t; + } + + int curLexState = 0; + int defaultLexState = 0; + int jjnewStateCnt; + int jjround; + int jjmatchedPos; + int jjmatchedKind; + + /** + * Get the next Token. + */ + public Token getNextToken() { + Token specialToken = null; + Token matchedToken; + int curPos = 0; + + EOFLoop: + for (; ; ) { + try { + curChar = inputStream.BeginToken(); + } catch (java.io.IOException e) { + jjmatchedKind = 0; + matchedToken = jjFillToken(); + matchedToken.specialToken = specialToken; + return matchedToken; + } + + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_0(); + if (jjmatchedKind != 0x7fffffff) { + if (jjmatchedPos + 1 < curPos) + inputStream.backup(curPos - jjmatchedPos - 1); + if ((JJ_TO_TOKEN[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) { + matchedToken = jjFillToken(); + matchedToken.specialToken = specialToken; + return matchedToken; + } else { + if ((JJ_TO_SPECIAL[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) { + matchedToken = jjFillToken(); + if (specialToken == null) + specialToken = matchedToken; + else { + matchedToken.specialToken = specialToken; + specialToken = specialToken.next = matchedToken; + } + } + continue EOFLoop; + } + } + int errorLine = inputStream.getEndLine(); + int errorColumn = inputStream.getEndColumn(); + String errorAfter = null; + boolean eofSeen = false; + try { + inputStream.readChar(); + inputStream.backup(1); + } catch (java.io.IOException e1) { + eofSeen = true; + errorAfter = curPos <= 1 ? "" : inputStream.GetImage(); + if (curChar == '\n' || curChar == '\r') { + errorLine++; + errorColumn = 0; + } else + errorColumn++; + } + if (!eofSeen) { + inputStream.backup(1); + errorAfter = curPos <= 1 ? "" : inputStream.GetImage(); + } + throw new TokenMgrError(eofSeen, curLexState, errorLine, errorColumn, errorAfter, curChar, TokenMgrError.LEXICAL_ERROR); + } + } + + private void jjCheckNAdd(int state) { + if (jjrounds[state] != jjround) { + jjstateSet[jjnewStateCnt++] = state; + jjrounds[state] = jjround; + } + } + + private void jjAddStates(int start, int end) { + do { + jjstateSet[jjnewStateCnt++] = JJ_NEXT_STATES[start]; + } while (start++ != end); + } + + private void jjCheckNAddTwoStates(int state1, int state2) { + jjCheckNAdd(state1); + jjCheckNAdd(state2); + } + + private void jjCheckNAddStates(int start, int end) { + do { + jjCheckNAdd(JJ_NEXT_STATES[start]); + } 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 new file mode 100644 index 0000000..b8e375e --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/parser/SimpleCharStream.java @@ -0,0 +1,504 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Generated By:JavaCC: Do not edit this line. SimpleCharStream.java Version 5.0 */ +/* 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). + */ + +public class SimpleCharStream { + /** + * Whether parser is static. + */ + public static final boolean STATIC_FLAG = false; + int bufsize; + int available; + int tokenBegin; + /** + * Position in buffer. + */ + public int bufpos = -1; + protected int[] bufline; + protected int[] bufcolumn; + + protected int column = 0; + protected int line = 1; + + protected boolean prevCharIsCR = false; + protected boolean prevCharIsLF = false; + + protected java.io.Reader inputStream; + + protected char[] buffer; + protected int maxNextCharInd = 0; + protected int inBuf = 0; + protected int tabSize = 8; + + protected void setTabSize(int i) { + tabSize = i; + } + + protected int getTabSize(int i) { + return tabSize; + } + + protected void ExpandBuff(boolean wrapAround) { + char[] newbuffer = new char[bufsize + 2048]; + int[] newbufline = new int[bufsize + 2048]; + int[] newbufcolumn = new int[bufsize + 2048]; + + try { + if (wrapAround) { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + System.arraycopy(buffer, 0, newbuffer, bufsize - tokenBegin, bufpos); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + System.arraycopy(bufline, 0, newbufline, bufsize - tokenBegin, bufpos); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + System.arraycopy(bufcolumn, 0, newbufcolumn, bufsize - tokenBegin, bufpos); + bufcolumn = newbufcolumn; + + maxNextCharInd = bufpos += bufsize - tokenBegin; + } else { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + bufcolumn = newbufcolumn; + + maxNextCharInd = bufpos -= tokenBegin; + } + } catch (Throwable t) { + throw new Error(t.getMessage()); + } + + bufsize += 2048; + available = bufsize; + tokenBegin = 0; + } + + protected void FillBuff() throws java.io.IOException { + if (maxNextCharInd == available) { + if (available == bufsize) { + if (tokenBegin > 2048) { + bufpos = maxNextCharInd = 0; + available = tokenBegin; + } else if (tokenBegin < 0) + bufpos = maxNextCharInd = 0; + else + ExpandBuff(false); + } else if (available > tokenBegin) + available = bufsize; + else if ((tokenBegin - available) < 2048) + ExpandBuff(true); + else + available = tokenBegin; + } + + int i; + try { + if ((i = inputStream.read(buffer, maxNextCharInd, available - maxNextCharInd)) == -1) { + inputStream.close(); + throw new java.io.IOException(); + } else + maxNextCharInd += i; + return; + } catch (java.io.IOException e) { + --bufpos; + backup(0); + if (tokenBegin == -1) + tokenBegin = bufpos; + throw e; + } + } + + /** + * Start. + */ + public char BeginToken() throws java.io.IOException { + tokenBegin = -1; + char c = readChar(); + tokenBegin = bufpos; + + return c; + } + + protected void UpdateLineColumn(char c) { + column++; + + if (prevCharIsLF) { + prevCharIsLF = false; + line += column = 1; + } else if (prevCharIsCR) { + prevCharIsCR = false; + if (c == '\n') { + prevCharIsLF = true; + } else + line += column = 1; + } + + switch (c) { + case '\r': + prevCharIsCR = true; + break; + case '\n': + prevCharIsLF = true; + break; + case '\t': + column--; + column += tabSize - (column % tabSize); + break; + default: + break; + } + + bufline[bufpos] = line; + bufcolumn[bufpos] = column; + } + + /** + * Read a character. + */ + public char readChar() throws java.io.IOException { + if (inBuf > 0) { + --inBuf; + + if (++bufpos == bufsize) + bufpos = 0; + + return buffer[bufpos]; + } + + if (++bufpos >= maxNextCharInd) + FillBuff(); + + char c = buffer[bufpos]; + + UpdateLineColumn(c); + return c; + } + + @Deprecated + /** + * @deprecated + * @see #getEndColumn + */ + + public int getColumn() { + return bufcolumn[bufpos]; + } + + @Deprecated + /** + * @deprecated + * @see #getEndLine + */ + + public int getLine() { + return bufline[bufpos]; + } + + /** + * Get token end column number. + */ + public int getEndColumn() { + return bufcolumn[bufpos]; + } + + /** + * Get token end line number. + */ + public int getEndLine() { + return bufline[bufpos]; + } + + /** + * Get token beginning column number. + */ + public int getBeginColumn() { + return bufcolumn[tokenBegin]; + } + + /** + * Get token beginning line number. + */ + public int getBeginLine() { + return bufline[tokenBegin]; + } + + /** + * Backup a number of characters. + */ + public void backup(int amount) { + + inBuf += amount; + if ((bufpos -= amount) < 0) + bufpos += bufsize; + } + + /** + * Constructor. + */ + public SimpleCharStream(java.io.Reader dstream, int startline, + int startcolumn, int buffersize) { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + } + + /** + * Constructor. + */ + public SimpleCharStream(java.io.Reader dstream, int startline, + int startcolumn) { + this(dstream, startline, startcolumn, 4096); + } + + /** + * Constructor. + */ + public SimpleCharStream(java.io.Reader dstream) { + this(dstream, 1, 1, 4096); + } + + /** + * Reinitialise. + */ + public void ReInit(java.io.Reader dstream, int startline, + int startcolumn, int buffersize) { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + if (buffer == null || buffersize != buffer.length) { + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + } + prevCharIsLF = prevCharIsCR = false; + tokenBegin = inBuf = maxNextCharInd = 0; + bufpos = -1; + } + + /** + * Reinitialise. + */ + public void ReInit(java.io.Reader dstream, int startline, + int startcolumn) { + ReInit(dstream, startline, startcolumn, 4096); + } + + /** + * Reinitialise. + */ + public void ReInit(java.io.Reader dstream) { + ReInit(dstream, 1, 1, 4096); + } + + /** + * Constructor. + */ + 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, StandardCharsets.UTF_8) : + new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + + /** + * Constructor. + */ + public SimpleCharStream(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) { + this(new java.io.InputStreamReader(dstream, StandardCharsets.UTF_8), startline, startcolumn, buffersize); + } + + /** + * Constructor. + */ + public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException { + this(dstream, encoding, startline, startcolumn, 4096); + } + + /** + * Constructor. + */ + public SimpleCharStream(java.io.InputStream dstream, int startline, + int startcolumn) { + this(dstream, startline, startcolumn, 4096); + } + + /** + * Constructor. + */ + public SimpleCharStream(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException { + this(dstream, encoding, 1, 1, 4096); + } + + /** + * Constructor. + */ + public SimpleCharStream(java.io.InputStream dstream) { + this(dstream, 1, 1, 4096); + } + + /** + * Reinitialise. + */ + 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, StandardCharsets.UTF_8) : + new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + + /** + * Reinitialise. + */ + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) { + ReInit(new java.io.InputStreamReader(dstream, StandardCharsets.UTF_8), startline, startcolumn, buffersize); + } + + /** + * Reinitialise. + */ + public void ReInit(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException { + ReInit(dstream, encoding, 1, 1, 4096); + } + + /** + * Reinitialise. + */ + public void ReInit(java.io.InputStream dstream) { + ReInit(dstream, 1, 1, 4096); + } + + /** + * Reinitialise. + */ + public void ReInit(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException { + ReInit(dstream, encoding, startline, startcolumn, 4096); + } + + /** + * Reinitialise. + */ + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn) { + ReInit(dstream, startline, startcolumn, 4096); + } + + /** + * Get token literal value. + */ + public String GetImage() { + if (bufpos >= tokenBegin) + return new String(buffer, tokenBegin, bufpos - tokenBegin + 1); + else + return new String(buffer, tokenBegin, bufsize - tokenBegin) + + new String(buffer, 0, bufpos + 1); + } + + /** + * Get the suffix. + */ + public char[] GetSuffix(int len) { + char[] ret = new char[len]; + + if ((bufpos + 1) >= len) + System.arraycopy(buffer, bufpos - len + 1, ret, 0, len); + else { + System.arraycopy(buffer, bufsize - (len - bufpos - 1), ret, 0, + len - bufpos - 1); + System.arraycopy(buffer, 0, ret, len - bufpos - 1, bufpos + 1); + } + + return ret; + } + + /** + * Reset buffer when finished. + */ + public void Done() { + buffer = null; + bufline = null; + bufcolumn = null; + } + + /** + * Method to adjust line and column numbers for the start of a token. + */ + public void adjustBeginLineColumn(int newLine, int newCol) { + int start = tokenBegin; + int len; + + if (bufpos >= tokenBegin) { + len = bufpos - tokenBegin + inBuf + 1; + } else { + len = bufsize - tokenBegin + bufpos + 1 + inBuf; + } + + int i = 0, j = 0, k = 0; + int nextColDiff = 0, columnDiff = 0; + + while (i < len && bufline[j = start % bufsize] == bufline[k = ++start % bufsize]) { + bufline[j] = newLine; + nextColDiff = columnDiff + bufcolumn[k] - bufcolumn[j]; + bufcolumn[j] = newCol + columnDiff; + columnDiff = nextColDiff; + i++; + } + + if (i < len) { + bufline[j] = newLine++; + bufcolumn[j] = newCol + columnDiff; + + while (i++ < len) { + if (bufline[j = start % bufsize] != bufline[++start % bufsize]) + bufline[j] = newLine++; + else + bufline[j] = newLine; + } + } + + line = bufline[j]; + column = bufcolumn[j]; + } + +} +/* 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 new file mode 100644 index 0000000..edb7880 --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/parser/Token.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. + */ + +/* Generated By:JavaCC: Do not edit this line. Token.java Version 5.0 */ +/* JavaCCOptions:TOKEN_EXTENDS=,KEEP_LINE_COL=null,SUPPORT_CLASS_VISIBILITY_PUBLIC=true */ +package org.apache.rocketmq.filter.parser; + +/** + * Describes the input token stream. + */ + +public class Token implements java.io.Serializable { + + /** + * The version identifier for this Serializable class. + * Increment only if the serialized form of the + * class changes. + */ + private static final long serialVersionUID = 1L; + + /** + * An integer that describes the kind of this token. This numbering + * system is determined by JavaCCParser, and a table of these numbers is + * stored in the file ...Constants.java. + */ + public int kind; + + /** + * The line number of the first character of this Token. + */ + public int beginLine; + /** + * The column number of the first character of this Token. + */ + public int beginColumn; + /** + * The line number of the last character of this Token. + */ + public int endLine; + /** + * The column number of the last character of this Token. + */ + public int endColumn; + + /** + * The string image of the token. + */ + public String image; + + /** + * A reference to the next regular (non-special) token from the input + * stream. If this is the last token from the input stream, or if the + * token manager has not read tokens beyond this one, this field is + * set to null. This is true only if this token is also a regular + * token. Otherwise, see below for a description of the contents of + * this field. + */ + public Token next; + + /** + * This field is used to access special tokens that occur prior to this + * token, but after the immediately preceding regular (non-special) token. + * If there are no such special tokens, this field is set to null. + * When there are more than one such special token, this field refers + * to the last of these special tokens, which in turn refers to the next + * previous special token through its specialToken field, and so on + * until the first special token (whose specialToken field is null). + * The next fields of special tokens refer to other special tokens that + * immediately follow it (without an intervening regular token). If there + * is no such token, this field is null. + */ + public Token specialToken; + + /** + * An optional attribute value of the Token. + * Tokens which are not used as syntactic sugar will often contain + * meaningful values that will be used later on by the compiler or + * interpreter. This attribute value is often different from the image. + * Any subclass of Token that actually wants to return a non-null value can + * override this method as appropriate. + */ + public Object getValue() { + return null; + } + + /** + * No-argument constructor + */ + public Token() { + } + + /** + * Constructs a new token for the specified Image. + */ + public Token(int kind) { + this(kind, null); + } + + /** + * Constructs a new token for the specified Image and Kind. + */ + public Token(int kind, String image) { + this.kind = kind; + this.image = image; + } + + /** + * Returns the image. + */ + public String toString() { + return image; + } + + /** + * Returns a new Token object, by default. However, if you want, you + * can create and return subclass objects based on the value of ofKind. + * Simply add the cases to the switch for all those special cases. + * For example, if you have a subclass of Token called IDToken that + * you want to create if ofKind is ID, simply add something like : + *

    + * case MyParserConstants.ID : return new IDToken(ofKind, image); + *

    + * to the following switch statement. Then you can cast matchedToken + * variable to the appropriate type and use sit in your lexical actions. + */ + public static Token newToken(int ofKind, String image) { + switch (ofKind) { + default: + return new Token(ofKind, image); + } + } + + public static Token newToken(int ofKind) { + return newToken(ofKind, null); + } + +} +/* 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 new file mode 100644 index 0000000..4a8f2c8 --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/parser/TokenMgrError.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. + */ + +/* Generated By:JavaCC: Do not edit this line. TokenMgrError.java Version 5.0 */ +/* JavaCCOptions: */ +package org.apache.rocketmq.filter.parser; + +/** + * Token Manager Error. + */ +public class TokenMgrError extends Error { + + /** + * The version identifier for this Serializable class. + * Increment only if the serialized form of the + * class changes. + */ + private static final long serialVersionUID = 1L; + + /* + * Ordinals for various reasons why an Error of this type can be thrown. + */ + + /** + * Lexical error occurred. + */ + static final int LEXICAL_ERROR = 0; + + /** + * An attempt was made to create a second instance of a static token manager. + */ + static final int STATIC_LEXER_ERROR = 1; + + /** + * Tried to change to an invalid lexical state. + */ + static final int INVALID_LEXICAL_STATE = 2; + + /** + * Detected (and bailed out of) an infinite loop in the token manager. + */ + static final int LOOP_DETECTED = 3; + + /** + * Indicates the reason why the exception is thrown. It will have + * one of the above 4 values. + */ + int errorCode; + + /** + * Replaces unprintable characters by their escaped (or unicode escaped) + * equivalents in the given string + */ + protected static final String addEscapes(String str) { + StringBuilder retval = new StringBuilder(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) { + case 0: + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + + /** + * Returns a detailed message for the Error when it is thrown by the + * token manager to indicate a lexical error. + * Parameters : + * eofSeen : indicates if EOF caused the lexical error + * curLexState : lexical state in which this error occurred + * errorLine : line number when the error occurred + * errorColumn : column number when the error occurred + * errorAfter : prefix that was seen before this error occurred + * curchar : the offending character + * Note: You can customize the lexical error message by modifying this method. + */ + protected static String LexicalError(boolean eofSeen, int lexState, int errorLine, int errorColumn, + String errorAfter, char curChar) { + return "Lexical error at line " + + errorLine + ", column " + + errorColumn + ". Encountered: " + + (eofSeen ? + " " : + ("\"" + addEscapes(String.valueOf(curChar)) + "\"") + " (" + (int) curChar + "), ") + + "after : \"" + addEscapes(errorAfter) + "\""; + } + + /** + * You can also modify the body of this method to customize your error messages. + * For example, cases like LOOP_DETECTED and INVALID_LEXICAL_STATE are not + * of end-users concern, so you can return something like : + *

    + * "Internal Error : Please file a bug report .... " + *

    + * from this method for such cases in the release version of your parser. + */ + @Override + public String getMessage() { + return super.getMessage(); + } + + /* + * Constructors of various flavors follow. + */ + + /** + * No arg constructor. + */ + public TokenMgrError() { + } + + /** + * Constructor with message and reason. + */ + public TokenMgrError(String message, int reason) { + super(message); + errorCode = reason; + } + + /** + * Full Constructor. + */ + public TokenMgrError(boolean eofSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar, + int reason) { + this(LexicalError(eofSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason); + } +} +/* JavaCC - OriginalChecksum=de79709675790dcbad2e0d728aa630d1 (do not edit this line) */ diff --git a/filter/src/main/java/org/apache/rocketmq/filter/util/BitsArray.java b/filter/src/main/java/org/apache/rocketmq/filter/util/BitsArray.java new file mode 100644 index 0000000..9866854 --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/util/BitsArray.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.filter.util; + +/** + * Wrapper of bytes array, in order to operate single bit easily. + */ +public class BitsArray implements Cloneable { + + private byte[] bytes; + private int bitLength; + + public static BitsArray create(int bitLength) { + return new BitsArray(bitLength); + } + + public static BitsArray create(byte[] bytes, int bitLength) { + return new BitsArray(bytes, bitLength); + } + + public static BitsArray create(byte[] bytes) { + return new BitsArray(bytes); + } + + private BitsArray(int bitLength) { + this.bitLength = bitLength; + // init bytes + int temp = bitLength / Byte.SIZE; + if (bitLength % Byte.SIZE > 0) { + temp++; + } + bytes = new byte[temp]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) 0x00; + } + } + + private BitsArray(byte[] bytes, int bitLength) { + if (bytes == null || bytes.length < 1) { + throw new IllegalArgumentException("Bytes is empty!"); + } + + if (bitLength < 1) { + throw new IllegalArgumentException("Bit is less than 1."); + } + + if (bitLength < bytes.length * Byte.SIZE) { + throw new IllegalArgumentException("BitLength is less than bytes.length() * " + Byte.SIZE); + } + + this.bytes = new byte[bytes.length]; + System.arraycopy(bytes, 0, this.bytes, 0, this.bytes.length); + this.bitLength = bitLength; + } + + private BitsArray(byte[] bytes) { + if (bytes == null || bytes.length < 1) { + throw new IllegalArgumentException("Bytes is empty!"); + } + + this.bitLength = bytes.length * Byte.SIZE; + this.bytes = new byte[bytes.length]; + System.arraycopy(bytes, 0, this.bytes, 0, this.bytes.length); + } + + public int bitLength() { + return this.bitLength; + } + + public int byteLength() { + return this.bytes.length; + } + + public byte[] bytes() { + return this.bytes; + } + + public void xor(final BitsArray other) { + checkInitialized(this); + checkInitialized(other); + + int minByteLength = Math.min(this.byteLength(), other.byteLength()); + + for (int i = 0; i < minByteLength; i++) { + this.bytes[i] = (byte) (this.bytes[i] ^ other.getByte(i)); + } + } + + public void xor(int bitPos, boolean set) { + checkBitPosition(bitPos, this); + + boolean value = getBit(bitPos); + if (value ^ set) { + setBit(bitPos, true); + } else { + setBit(bitPos, false); + } + } + + public void or(final BitsArray other) { + checkInitialized(this); + checkInitialized(other); + + int minByteLength = Math.min(this.byteLength(), other.byteLength()); + + for (int i = 0; i < minByteLength; i++) { + this.bytes[i] = (byte) (this.bytes[i] | other.getByte(i)); + } + } + + public void or(int bitPos, boolean set) { + checkBitPosition(bitPos, this); + + if (set) { + setBit(bitPos, true); + } + } + + public void and(final BitsArray other) { + checkInitialized(this); + checkInitialized(other); + + int minByteLength = Math.min(this.byteLength(), other.byteLength()); + + for (int i = 0; i < minByteLength; i++) { + this.bytes[i] = (byte) (this.bytes[i] & other.getByte(i)); + } + } + + public void and(int bitPos, boolean set) { + checkBitPosition(bitPos, this); + + if (!set) { + setBit(bitPos, false); + } + } + + public void not(int bitPos) { + checkBitPosition(bitPos, this); + + setBit(bitPos, !getBit(bitPos)); + } + + public void setBit(int bitPos, boolean set) { + checkBitPosition(bitPos, this); + int sub = subscript(bitPos); + int pos = position(bitPos); + if (set) { + this.bytes[sub] = (byte) (this.bytes[sub] | pos); + } else { + this.bytes[sub] = (byte) (this.bytes[sub] & ~pos); + } + } + + public void setByte(int bytePos, byte set) { + checkBytePosition(bytePos, this); + + this.bytes[bytePos] = set; + } + + public boolean getBit(int bitPos) { + checkBitPosition(bitPos, this); + + return (this.bytes[subscript(bitPos)] & position(bitPos)) != 0; + } + + public byte getByte(int bytePos) { + checkBytePosition(bytePos, this); + + return this.bytes[bytePos]; + } + + protected int subscript(int bitPos) { + return bitPos / Byte.SIZE; + } + + protected int position(int bitPos) { + return 1 << bitPos % Byte.SIZE; + } + + protected void checkBytePosition(int bytePos, BitsArray bitsArray) { + checkInitialized(bitsArray); + if (bytePos > bitsArray.byteLength()) { + throw new IllegalArgumentException("BytePos is greater than " + bytes.length); + } + if (bytePos < 0) { + throw new IllegalArgumentException("BytePos is less than 0"); + } + } + + protected void checkBitPosition(int bitPos, BitsArray bitsArray) { + checkInitialized(bitsArray); + if (bitPos > bitsArray.bitLength()) { + throw new IllegalArgumentException("BitPos is greater than " + bitLength); + } + if (bitPos < 0) { + throw new IllegalArgumentException("BitPos is less than 0"); + } + } + + protected void checkInitialized(BitsArray bitsArray) { + if (bitsArray.bytes() == null) { + throw new RuntimeException("Not initialized!"); + } + } + + public BitsArray clone() { + byte[] clone = new byte[this.byteLength()]; + + System.arraycopy(this.bytes, 0, clone, 0, this.byteLength()); + + return create(clone, bitLength()); + } + + @Override + public String toString() { + if (this.bytes == null) { + return "null"; + } + StringBuilder stringBuilder = new StringBuilder(this.bytes.length * Byte.SIZE); + for (int i = this.bytes.length - 1; i >= 0; i--) { + + int j = Byte.SIZE - 1; + if (i == this.bytes.length - 1 && this.bitLength % Byte.SIZE > 0) { + // not full byte + j = this.bitLength % Byte.SIZE; + } + + for (; j >= 0; j--) { + + byte mask = (byte) (1 << j); + if ((this.bytes[i] & mask) == mask) { + stringBuilder.append("1"); + } else { + stringBuilder.append("0"); + } + } + if (i % 8 == 0) { + stringBuilder.append("\n"); + } + } + + return stringBuilder.toString(); + } +} 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 new file mode 100644 index 0000000..d909cc2 --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/util/BloomFilter.java @@ -0,0 +1,304 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.util; + +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 = StandardCharsets.UTF_8; + + // as error rate, 10/100 = 0.1 + private int f = 10; + private int n = 128; + + // hash function num, by calculation. + private int k; + // bit count, by calculation. + private int m; + + /** + * Create bloom filter by error rate and mapping num. + * + * @param f error rate + * @param n num will mapping to bit + */ + public static BloomFilter createByFn(int f, int n) { + return new BloomFilter(f, n); + } + + /** + * Constructor. + * + * @param f error rate + * @param n num will mapping to bit + */ + private BloomFilter(int f, int n) { + if (f < 1 || f >= 100) { + throw new IllegalArgumentException("f must be greater or equal than 1 and less than 100"); + } + if (n < 1) { + throw new IllegalArgumentException("n must be greater than 0"); + } + + this.f = f; + this.n = n; + + // set p = e^(-kn/m) + // f = (1 - p)^k = e^(kln(1-p)) + // when p = 0.5, k = ln2 * (m/n), f = (1/2)^k = (0.618)^(m/n) + double errorRate = f / 100.0; + this.k = (int) Math.ceil(logMN(0.5, errorRate)); + + if (this.k < 1) { + throw new IllegalArgumentException("Hash function num is less than 1, maybe you should change the value of error rate or bit num!"); + } + + // m >= n*log2(1/f)*log2(e) + this.m = (int) Math.ceil(this.n * logMN(2, 1 / errorRate) * logMN(2, Math.E)); + // m%8 = 0 + this.m = (int) (Byte.SIZE * Math.ceil(this.m / (Byte.SIZE * 1.0))); + } + + /** + * Calculate bit positions of {@code str}. + *

    + * See "Less Hashing, Same Performance: Building a Better Bloom Filter" by Adam Kirsch and Michael + * Mitzenmacher. + *

    + */ + public int[] calcBitPositions(String str) { + int[] bitPositions = new int[this.k]; + + long hash64 = Hashing.murmur3_128().hashString(str, UTF_8).asLong(); + + int hash1 = (int) hash64; + int hash2 = (int) (hash64 >>> 32); + + for (int i = 1; i <= this.k; i++) { + int combinedHash = hash1 + (i * hash2); + // Flip all the bits if it's negative (guaranteed positive number) + if (combinedHash < 0) { + combinedHash = ~combinedHash; + } + bitPositions[i - 1] = combinedHash % this.m; + } + + return bitPositions; + } + + /** + * Calculate bit positions of {@code str} to construct {@code BloomFilterData} + */ + public BloomFilterData generate(String str) { + int[] bitPositions = calcBitPositions(str); + + return new BloomFilterData(bitPositions, this.m); + } + + /** + * Calculate bit positions of {@code str}, then set the related {@code bits} positions to 1. + */ + public void hashTo(String str, BitsArray bits) { + hashTo(calcBitPositions(str), bits); + } + + /** + * Set the related {@code bits} positions to 1. + */ + public void hashTo(int[] bitPositions, BitsArray bits) { + check(bits); + + for (int i : bitPositions) { + bits.setBit(i, true); + } + } + + /** + * Extra check: + *
  • 1. check {@code filterData} belong to this bloom filter.
  • + *

    + * Then set the related {@code bits} positions to 1. + *

    + */ + 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) + ); + } + hashTo(filterData.getBitPos(), bits); + } + + /** + * Calculate bit positions of {@code str}, then check all the related {@code bits} positions is 1. + * + * @return true: all the related {@code bits} positions is 1 + */ + public boolean isHit(String str, BitsArray bits) { + return isHit(calcBitPositions(str), bits); + } + + /** + * Check all the related {@code bits} positions is 1. + * + * @return true: all the related {@code bits} positions is 1 + */ + public boolean isHit(int[] bitPositions, BitsArray bits) { + check(bits); + boolean ret = bits.getBit(bitPositions[0]); + for (int i = 1; i < bitPositions.length; i++) { + ret &= bits.getBit(bitPositions[i]); + } + return ret; + } + + /** + * Check all the related {@code bits} positions is 1. + * + * @return true: all the related {@code bits} positions is 1 + */ + 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) + ); + } + return isHit(filterData.getBitPos(), bits); + } + + /** + * Check whether one of {@code bitPositions} has been occupied. + * + * @return true: if all positions have been occupied. + */ + public boolean checkFalseHit(int[] bitPositions, BitsArray bits) { + for (int j = 0; j < bitPositions.length; j++) { + int pos = bitPositions[j]; + + // check position of bits has been set. + // that mean no one occupy the position. + if (!bits.getBit(pos)) { + return false; + } + } + + return true; + } + + protected void check(BitsArray bits) { + if (bits.bitLength() != this.m) { + throw new IllegalArgumentException( + String.format("Length(%d) of bits in BitsArray is not equal to %d!", bits.bitLength(), this.m) + ); + } + } + + /** + * Check {@code BloomFilterData} is valid, and belong to this bloom filter. + *
  • 1. not null
  • + *
  • 2. {@link org.apache.rocketmq.filter.util.BloomFilterData#getBitNum} must be equal to {@code m}
  • + *
  • 3. {@link org.apache.rocketmq.filter.util.BloomFilterData#getBitPos} is not null
  • + *
  • 4. {@link org.apache.rocketmq.filter.util.BloomFilterData#getBitPos}'s length is equal to {@code k}
  • + */ + public boolean isValid(BloomFilterData filterData) { + if (filterData == null + || filterData.getBitNum() != this.m + || filterData.getBitPos() == null + || filterData.getBitPos().length != this.k) { + return false; + } + + return true; + } + + /** + * error rate. + */ + public int getF() { + return f; + } + + /** + * expect mapping num. + */ + public int getN() { + return n; + } + + /** + * hash function num. + */ + public int getK() { + return k; + } + + /** + * total bit num. + */ + public int getM() { + return m; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof BloomFilter)) + return false; + + BloomFilter that = (BloomFilter) o; + + if (f != that.f) + return false; + if (k != that.k) + return false; + if (m != that.m) + return false; + if (n != that.n) + return false; + + return true; + } + + @Override + public int hashCode() { + int result = f; + result = 31 * result + n; + result = 31 * result + k; + result = 31 * result + m; + return result; + } + + @Override + public String toString() { + return String.format("f: %d, n: %d, k: %d, m: %d", f, n, k, m); + } + + protected double logMN(double m, double n) { + return Math.log(n) / Math.log(m); + } +} diff --git a/filter/src/main/java/org/apache/rocketmq/filter/util/BloomFilterData.java b/filter/src/main/java/org/apache/rocketmq/filter/util/BloomFilterData.java new file mode 100644 index 0000000..c021323 --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/util/BloomFilterData.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.filter.util; + +import java.util.Arrays; + +/** + * Data generated by bloom filter, include: + *
  • 1. Bit positions allocated to requester;
  • + *
  • 2. Total bit num when allocating;
  • + */ +public class BloomFilterData { + + private int[] bitPos; + private int bitNum; + + public BloomFilterData() { + } + + public BloomFilterData(int[] bitPos, int bitNum) { + this.bitPos = bitPos; + this.bitNum = bitNum; + } + + public int[] getBitPos() { + return bitPos; + } + + public int getBitNum() { + return bitNum; + } + + public void setBitPos(final int[] bitPos) { + this.bitPos = bitPos; + } + + public void setBitNum(final int bitNum) { + this.bitNum = bitNum; + } + + @Override + public boolean equals(final Object o) { + if (this == o) + return true; + if (!(o instanceof BloomFilterData)) + return false; + + final BloomFilterData that = (BloomFilterData) o; + + if (bitNum != that.bitNum) + return false; + if (!Arrays.equals(bitPos, that.bitPos)) + return false; + + return true; + } + + @Override + public int hashCode() { + int result = bitPos != null ? Arrays.hashCode(bitPos) : 0; + result = 31 * result + bitNum; + return result; + } + + @Override + public String toString() { + return "BloomFilterData{" + + "bitPos=" + Arrays.toString(bitPos) + + ", bitNum=" + bitNum + + '}'; + } +} diff --git a/filter/src/test/java/org/apache/rocketmq/filter/BitsArrayTest.java b/filter/src/test/java/org/apache/rocketmq/filter/BitsArrayTest.java new file mode 100644 index 0000000..1ba7360 --- /dev/null +++ b/filter/src/test/java/org/apache/rocketmq/filter/BitsArrayTest.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.filter; + +import org.apache.rocketmq.filter.util.BitsArray; +import org.junit.Test; + +import java.util.Random; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BitsArrayTest { + + BitsArray gen(int bitCount) { + BitsArray bitsArray = BitsArray.create(bitCount); + + for (int i = 0; i < bitCount / Byte.SIZE; i++) { + bitsArray.setByte(i, (byte) (new Random(System.currentTimeMillis())).nextInt(0xff)); + try { + Thread.sleep(2); + } catch (InterruptedException e) { + } + } + + return bitsArray; + } + + int bitLength = Byte.SIZE; + + @Test + public void testConstructor() { + BitsArray bitsArray = BitsArray.create(8); + + assertThat(bitsArray.byteLength() == 1 && bitsArray.bitLength() == 8).isTrue(); + + bitsArray = BitsArray.create(9); + + assertThat(bitsArray.byteLength() == 2 && bitsArray.bitLength() == 9).isTrue(); + + bitsArray = BitsArray.create(7); + + assertThat(bitsArray.byteLength() == 1 && bitsArray.bitLength() == 7).isTrue(); + } + + @Test + public void testSet() { + BitsArray bitsArray = gen(bitLength); + BitsArray backUp = bitsArray.clone(); + + boolean val = bitsArray.getBit(2); + + bitsArray.setBit(2, !val); + + bitsArray.xor(backUp); + + assertThat(bitsArray.getBit(2)).isTrue(); + } + + @Test + public void testAndOr() { + BitsArray bitsArray = gen(bitLength); + + boolean val = bitsArray.getBit(2); + + if (val) { + bitsArray.and(2, false); + assertThat(!bitsArray.getBit(2)).isTrue(); + } else { + bitsArray.or(2, true); + assertThat(bitsArray.getBit(2)).isTrue(); + } + } + + @Test + public void testXor() { + BitsArray bitsArray = gen(bitLength); + + boolean val = bitsArray.getBit(2); + + bitsArray.xor(2, !val); + + assertThat(bitsArray.getBit(2)).isTrue(); + } + + @Test + public void testNot() { + BitsArray bitsArray = gen(bitLength); + BitsArray backUp = bitsArray.clone(); + + bitsArray.not(2); + + bitsArray.xor(backUp); + + assertThat(bitsArray.getBit(2)).isTrue(); + } + + @Test + public void testOr() { + BitsArray b1 = BitsArray.create(new byte[] {(byte) 0xff, 0x00}); + BitsArray b2 = BitsArray.create(new byte[] {0x00, (byte) 0xff}); + + b1.or(b2); + + for (int i = 0; i < b1.bitLength(); i++) { + assertThat(b1.getBit(i)).isTrue(); + } + } +} diff --git a/filter/src/test/java/org/apache/rocketmq/filter/BloomFilterTest.java b/filter/src/test/java/org/apache/rocketmq/filter/BloomFilterTest.java new file mode 100644 index 0000000..d2425b4 --- /dev/null +++ b/filter/src/test/java/org/apache/rocketmq/filter/BloomFilterTest.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.filter; + +import org.apache.rocketmq.filter.util.BitsArray; +import org.apache.rocketmq.filter.util.BloomFilter; +import org.apache.rocketmq.filter.util.BloomFilterData; +import org.junit.Test; + +import java.util.Random; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BloomFilterTest { + + @Test + public void testEquals() { + BloomFilter a = BloomFilter.createByFn(10, 20); + + BloomFilter b = BloomFilter.createByFn(10, 20); + + BloomFilter c = BloomFilter.createByFn(12, 20); + + BloomFilter d = BloomFilter.createByFn(10, 30); + + assertThat(a).isEqualTo(b); + assertThat(a).isNotEqualTo(c); + assertThat(a).isNotEqualTo(d); + assertThat(d).isNotEqualTo(c); + + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + assertThat(a.hashCode()).isNotEqualTo(c.hashCode()); + assertThat(a.hashCode()).isNotEqualTo(d.hashCode()); + assertThat(c.hashCode()).isNotEqualTo(d.hashCode()); + } + + @Test + public void testHashTo() { + String cid = "CID_abc_efg"; + + BloomFilter bloomFilter = BloomFilter.createByFn(10, 20); + + BitsArray bits = BitsArray.create(bloomFilter.getM()); + + int[] bitPos = bloomFilter.calcBitPositions(cid); + + bloomFilter.hashTo(cid, bits); + + for (int bit : bitPos) { + assertThat(bits.getBit(bit)).isTrue(); + } + } + + @Test + public void testCalcBitPositions() { + String cid = "CID_abc_efg"; + + BloomFilter bloomFilter = BloomFilter.createByFn(10, 20); + + int[] bitPos = bloomFilter.calcBitPositions(cid); + + assertThat(bitPos).isNotNull(); + assertThat(bitPos.length).isEqualTo(bloomFilter.getK()); + + int[] bitPos2 = bloomFilter.calcBitPositions(cid); + + assertThat(bitPos2).isNotNull(); + assertThat(bitPos2.length).isEqualTo(bloomFilter.getK()); + + assertThat(bitPos).isEqualTo(bitPos2); + } + + @Test + public void testIsHit() { + String cid = "CID_abc_efg"; + String cid2 = "CID_abc_123"; + + BloomFilter bloomFilter = BloomFilter.createByFn(10, 20); + + BitsArray bits = BitsArray.create(bloomFilter.getM()); + + bloomFilter.hashTo(cid, bits); + + assertThat(bloomFilter.isHit(cid, bits)).isTrue(); + assertThat(!bloomFilter.isHit(cid2, bits)).isTrue(); + + bloomFilter.hashTo(cid2, bits); + + assertThat(bloomFilter.isHit(cid, bits)).isTrue(); + assertThat(bloomFilter.isHit(cid2, bits)).isTrue(); + } + + @Test + public void testBloomFilterData() { + BloomFilterData bloomFilterData = new BloomFilterData(new int[] {1, 2, 3}, 128); + BloomFilterData bloomFilterData1 = new BloomFilterData(new int[] {1, 2, 3}, 128); + BloomFilterData bloomFilterData2 = new BloomFilterData(new int[] {1, 2, 3}, 129); + + assertThat(bloomFilterData).isEqualTo(bloomFilterData1); + assertThat(bloomFilterData2).isNotEqualTo(bloomFilterData); + assertThat(bloomFilterData2).isNotEqualTo(bloomFilterData1); + + assertThat(bloomFilterData.hashCode()).isEqualTo(bloomFilterData1.hashCode()); + assertThat(bloomFilterData2.hashCode()).isNotEqualTo(bloomFilterData.hashCode()); + assertThat(bloomFilterData2.hashCode()).isNotEqualTo(bloomFilterData1.hashCode()); + + assertThat(bloomFilterData.getBitPos()).isEqualTo(bloomFilterData2.getBitPos()); + assertThat(bloomFilterData.getBitNum()).isEqualTo(bloomFilterData1.getBitNum()); + assertThat(bloomFilterData.getBitNum()).isNotEqualTo(bloomFilterData2.getBitNum()); + + bloomFilterData2.setBitNum(128); + + assertThat(bloomFilterData).isEqualTo(bloomFilterData2); + + bloomFilterData2.setBitPos(new int[] {1, 2, 3, 4}); + + assertThat(bloomFilterData).isNotEqualTo(bloomFilterData2); + + BloomFilterData nullData = new BloomFilterData(); + + assertThat(nullData.getBitNum()).isEqualTo(0); + assertThat(nullData.getBitPos()).isNull(); + + BloomFilter bloomFilter = BloomFilter.createByFn(1, 300); + + assertThat(bloomFilter).isNotNull(); + assertThat(bloomFilter.isValid(bloomFilterData)).isFalse(); + } + + @Test + public void testCheckFalseHit() { + BloomFilter bloomFilter = BloomFilter.createByFn(1, 300); + BitsArray bits = BitsArray.create(bloomFilter.getM()); + int falseHit = 0; + for (int i = 0; i < bloomFilter.getN(); i++) { + String str = randomString((new Random(System.nanoTime())).nextInt(127) + 10); + int[] bitPos = bloomFilter.calcBitPositions(str); + + if (bloomFilter.checkFalseHit(bitPos, bits)) { + falseHit++; + } + + bloomFilter.hashTo(bitPos, bits); + } + + assertThat(falseHit).isLessThanOrEqualTo(bloomFilter.getF() * bloomFilter.getN() / 100); + } + + private String randomString(int length) { + StringBuilder stringBuilder = new StringBuilder(length); + for (int i = 0; i < length; i++) { + stringBuilder.append((char) ((new Random(System.nanoTime())).nextInt(123 - 97) + 97)); + } + + return stringBuilder.toString(); + } +} diff --git a/filter/src/test/java/org/apache/rocketmq/filter/ExpressionTest.java b/filter/src/test/java/org/apache/rocketmq/filter/ExpressionTest.java new file mode 100644 index 0000000..df88345 --- /dev/null +++ b/filter/src/test/java/org/apache/rocketmq/filter/ExpressionTest.java @@ -0,0 +1,958 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; + +import org.apache.rocketmq.filter.expression.ComparisonExpression; +import org.apache.rocketmq.filter.expression.ConstantExpression; +import org.apache.rocketmq.filter.expression.EvaluationContext; +import org.apache.rocketmq.filter.expression.Expression; +import org.apache.rocketmq.filter.expression.MQFilterException; +import org.apache.rocketmq.filter.expression.PropertyExpression; +import org.apache.rocketmq.filter.parser.SelectorParser; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ExpressionTest { + + private static String andExpression = "a=3 and b<>4 And c>5 AND d<=4"; + private static String orExpression = "a=3 or b<>4 Or c>5 OR d<=4"; + private static String inExpression = "a in ('3', '4', '5')"; + private static String notInExpression = "a not in ('3', '4', '5')"; + private static String betweenExpression = "a between 2 and 10"; + private static String notBetweenExpression = "a not between 2 and 10"; + private static String isNullExpression = "a is null"; + private static String isNotNullExpression = "a is not null"; + private static String equalExpression = "a is not null and a='hello'"; + private static String booleanExpression = "a=TRUE OR b=FALSE"; + 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); + + EvaluationContext context = genContext( + KeyValue.c("TAGS", "''tag''") + ); + + eval(expr, context, Boolean.TRUE); + } + + @Test + public void testEvaluate_now() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("a", System.currentTimeMillis()) + ); + + Expression nowExpression = ConstantExpression.createNow(); + Expression propertyExpression = new PropertyExpression("a"); + + Expression expression = ComparisonExpression.createLessThanEqual(propertyExpression, + nowExpression); + + eval(expression, context, Boolean.TRUE); + } + + @Test(expected = RuntimeException.class) + public void testEvaluate_stringCompare() throws Exception { + Expression expression = genExp("a between up and low"); + + EvaluationContext context = genContext( + KeyValue.c("a", "3.14") + ); + + eval(expression, context, Boolean.FALSE); + + { + context = genContext( + KeyValue.c("a", "3.14"), + KeyValue.c("up", "up"), + KeyValue.c("low", "low") + ); + + eval(expression, context, Boolean.FALSE); + } + + { + expression = genExp("key is not null and key between 0 and 100"); + + context = genContext( + KeyValue.c("key", "con") + ); + + eval(expression, context, Boolean.FALSE); + } + + { + expression = genExp("a between 0 and 100"); + + context = genContext( + KeyValue.c("a", "abc") + ); + + eval(expression, context, Boolean.FALSE); + } + + { + expression = genExp("a=b"); + + context = genContext( + KeyValue.c("a", "3.14"), + KeyValue.c("b", "3.14") + ); + + eval(expression, context, Boolean.TRUE); + } + + { + expression = genExp("a<>b"); + + context = genContext( + KeyValue.c("a", "3.14"), + KeyValue.c("b", "3.14") + ); + + eval(expression, context, Boolean.FALSE); + } + + { + expression = genExp("a<>b"); + + context = genContext( + KeyValue.c("a", "3.14"), + KeyValue.c("b", "3.141") + ); + + eval(expression, context, Boolean.TRUE); + } + } + + @Test + public void testEvaluate_exponent() throws Exception { + Expression expression = genExp("a > 3.1E10"); + + EvaluationContext context = genContext( + KeyValue.c("a", String.valueOf(3.1415 * Math.pow(10, 10))) + ); + + eval(expression, context, Boolean.TRUE); + } + + @Test + public void testEvaluate_floatNumber() throws Exception { + Expression expression = genExp("a > 3.14"); + + EvaluationContext context = genContext( + KeyValue.c("a", String.valueOf(3.1415)) + ); + + eval(expression, context, Boolean.TRUE); + } + + @Test + public void testEvaluate_twoVariable() throws Exception { + Expression expression = genExp("a > b"); + + EvaluationContext context = genContext( + KeyValue.c("a", String.valueOf(10)), + KeyValue.c("b", String.valueOf(20)) + ); + + eval(expression, context, Boolean.FALSE); + } + + @Test + public void testEvaluate_twoVariableGt() throws Exception { + Expression expression = genExp("a > b"); + EvaluationContext context = genContext( + KeyValue.c("b", String.valueOf(10)), + KeyValue.c("a", String.valueOf(20)) + ); + + eval(expression, context, Boolean.TRUE); + } + + @Test + public void testEvaluate_nullOr() throws Exception { + Expression expression = genExp(nullOrExpression); + + EvaluationContext context = genContext( + ); + + eval(expression, context, Boolean.TRUE); + + context = genContext( + KeyValue.c("a", "hello") + ); + + eval(expression, context, Boolean.TRUE); + + context = genContext( + KeyValue.c("a", "abc") + ); + + eval(expression, context, Boolean.FALSE); + } + + @Test + public void testEvaluate_boolean() throws Exception { + Expression expression = genExp(booleanExpression); + + EvaluationContext context = genContext( + KeyValue.c("a", "true"), + KeyValue.c("b", "false") + ); + + eval(expression, context, Boolean.TRUE); + + context = genContext( + KeyValue.c("a", "false"), + KeyValue.c("b", "true") + ); + + eval(expression, context, Boolean.FALSE); + } + + @Test + public void testEvaluate_equal() throws Exception { + Expression expression = genExp(equalExpression); + + EvaluationContext context = genContext( + KeyValue.c("a", "hello") + ); + + eval(expression, context, Boolean.TRUE); + + context = genContext( + ); + + eval(expression, context, Boolean.FALSE); + } + + @Test + public void testEvaluate_andTrue() throws Exception { + Expression expression = genExp(andExpression); + + EvaluationContext context = genContext( + KeyValue.c("a", 3), + KeyValue.c("b", 5), + KeyValue.c("c", 6), + KeyValue.c("d", 1) + ); + + for (int i = 0; i < 500; i++) { + eval(expression, context, Boolean.TRUE); + } + + long start = System.currentTimeMillis(); + for (int j = 0; j < 100; j++) { + for (int i = 0; i < 1000; i++) { + eval(expression, context, Boolean.TRUE); + } + } + + // use string + context = genContext( + KeyValue.c("a", "3"), + KeyValue.c("b", "5"), + KeyValue.c("c", "6"), + KeyValue.c("d", "1") + ); + + eval(expression, context, Boolean.TRUE); + } + + @Test + public void testEvaluate_andFalse() throws Exception { + Expression expression = genExp(andExpression); + + EvaluationContext context = genContext( + KeyValue.c("a", 4), + KeyValue.c("b", 5), + KeyValue.c("c", 6), + KeyValue.c("d", 1) + ); + + eval(expression, context, Boolean.FALSE); + + // use string + context = genContext( + KeyValue.c("a", "4"), + KeyValue.c("b", "5"), + KeyValue.c("c", "6"), + KeyValue.c("d", "1") + ); + + eval(expression, context, Boolean.FALSE); + } + + @Test + public void testEvaluate_orTrue() throws Exception { + Expression expression = genExp(orExpression); + + // first + EvaluationContext context = genContext( + KeyValue.c("a", 3) + ); + eval(expression, context, Boolean.TRUE); + + // second + context = genContext( + KeyValue.c("a", 4), + KeyValue.c("b", 5) + ); + eval(expression, context, Boolean.TRUE); + + // third + context = genContext( + KeyValue.c("a", 4), + KeyValue.c("b", 4), + KeyValue.c("c", 6) + ); + eval(expression, context, Boolean.TRUE); + + // forth + context = genContext( + KeyValue.c("a", 4), + KeyValue.c("b", 4), + KeyValue.c("c", 3), + KeyValue.c("d", 2) + ); + eval(expression, context, Boolean.TRUE); + } + + @Test + public void testEvaluate_orFalse() throws Exception { + Expression expression = genExp(orExpression); + // forth + EvaluationContext context = genContext( + KeyValue.c("a", 4), + KeyValue.c("b", 4), + KeyValue.c("c", 3), + KeyValue.c("d", 10) + ); + eval(expression, context, Boolean.FALSE); + } + + @Test + public void testEvaluate_inTrue() throws Exception { + Expression expression = genExp(inExpression); + + EvaluationContext context = genContext( + KeyValue.c("a", "3") + ); + eval(expression, context, Boolean.TRUE); + + context = genContext( + KeyValue.c("a", "4") + ); + eval(expression, context, Boolean.TRUE); + + context = genContext( + KeyValue.c("a", "5") + ); + eval(expression, context, Boolean.TRUE); + } + + @Test + public void testEvaluate_inFalse() throws Exception { + Expression expression = genExp(inExpression); + + EvaluationContext context = genContext( + KeyValue.c("a", "8") + ); + eval(expression, context, Boolean.FALSE); + } + + @Test + public void testEvaluate_notInTrue() throws Exception { + Expression expression = genExp(notInExpression); + + EvaluationContext context = genContext( + KeyValue.c("a", "8") + ); + eval(expression, context, Boolean.TRUE); + } + + @Test + public void testEvaluate_notInFalse() throws Exception { + Expression expression = genExp(notInExpression); + + EvaluationContext context = genContext( + KeyValue.c("a", "3") + ); + eval(expression, context, Boolean.FALSE); + + context = genContext( + KeyValue.c("a", "4") + ); + eval(expression, context, Boolean.FALSE); + + context = genContext( + KeyValue.c("a", "5") + ); + eval(expression, context, Boolean.FALSE); + } + + @Test + public void testEvaluate_betweenTrue() throws Exception { + Expression expression = genExp(betweenExpression); + + EvaluationContext context = genContext( + KeyValue.c("a", "2") + ); + eval(expression, context, Boolean.TRUE); + + context = genContext( + KeyValue.c("a", "10") + ); + eval(expression, context, Boolean.TRUE); + + context = genContext( + KeyValue.c("a", "3") + ); + eval(expression, context, Boolean.TRUE); + } + + @Test + public void testEvaluate_betweenFalse() throws Exception { + Expression expression = genExp(betweenExpression); + + EvaluationContext context = genContext( + KeyValue.c("a", "1") + ); + eval(expression, context, Boolean.FALSE); + + context = genContext( + KeyValue.c("a", "11") + ); + eval(expression, context, Boolean.FALSE); + } + + @Test + public void testEvaluate_notBetweenTrue() throws Exception { + Expression expression = genExp(notBetweenExpression); + + EvaluationContext context = genContext( + KeyValue.c("a", "1") + ); + eval(expression, context, Boolean.TRUE); + + context = genContext( + KeyValue.c("a", "11") + ); + eval(expression, context, Boolean.TRUE); + } + + @Test + public void testEvaluate_notBetweenFalse() throws Exception { + Expression expression = genExp(notBetweenExpression); + + EvaluationContext context = genContext( + KeyValue.c("a", "2") + ); + eval(expression, context, Boolean.FALSE); + + context = genContext( + KeyValue.c("a", "10") + ); + eval(expression, context, Boolean.FALSE); + + context = genContext( + KeyValue.c("a", "3") + ); + eval(expression, context, Boolean.FALSE); + } + + @Test + public void testEvaluate_isNullTrue() throws Exception { + Expression expression = genExp(isNullExpression); + + EvaluationContext context = genContext( + KeyValue.c("abc", "2") + ); + eval(expression, context, Boolean.TRUE); + } + + @Test + public void testEvaluate_isNullFalse() throws Exception { + Expression expression = genExp(isNullExpression); + + EvaluationContext context = genContext( + KeyValue.c("a", "2") + ); + eval(expression, context, Boolean.FALSE); + } + + @Test + public void testEvaluate_isNotNullTrue() throws Exception { + Expression expression = genExp(isNotNullExpression); + + EvaluationContext context = genContext( + KeyValue.c("a", "2") + ); + eval(expression, context, Boolean.TRUE); + } + + @Test + public void testEvaluate_isNotNullFalse() throws Exception { + Expression expression = genExp(isNotNullExpression); + + EvaluationContext context = genContext( + KeyValue.c("abc", "2") + ); + eval(expression, context, Boolean.FALSE); + } + + protected void eval(Expression expression, EvaluationContext context, Boolean result) throws Exception { + Object ret = expression.evaluate(context); + if (ret == null || !(ret instanceof Boolean)) { + assertThat(result).isFalse(); + } else { + assertThat(result).isEqualTo(ret); + } + } + + protected EvaluationContext genContext(KeyValue... keyValues) { + if (keyValues == null || keyValues.length < 1) { + return new PropertyContext(); + } + + PropertyContext context = new PropertyContext(); + for (KeyValue keyValue : keyValues) { + context.properties.put(keyValue.key, keyValue.value); + } + + return context; + } + + protected Expression genExp(String exp) { + Expression expression = null; + + try { + expression = SelectorParser.parse(exp); + + assertThat(expression).isNotNull(); + } catch (MQFilterException e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + + return expression; + } + + static class KeyValue { + public static KeyValue c(String key, Object value) { + return new KeyValue(key, value); + } + + public KeyValue(String key, Object value) { + this.key = key; + this.value = value; + } + + public String key; + public Object value; + } + + class PropertyContext implements EvaluationContext { + + public Map properties = new HashMap<>(8); + + @Override + public Object get(final String name) { + return properties.get(name); + } + + @Override + public Map keyValues() { + return properties; + } + + } +} diff --git a/filter/src/test/java/org/apache/rocketmq/filter/FilterSpiTest.java b/filter/src/test/java/org/apache/rocketmq/filter/FilterSpiTest.java new file mode 100644 index 0000000..25f8cfd --- /dev/null +++ b/filter/src/test/java/org/apache/rocketmq/filter/FilterSpiTest.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.filter; + +import org.apache.rocketmq.common.filter.ExpressionType; +import org.apache.rocketmq.filter.expression.EmptyEvaluationContext; +import org.apache.rocketmq.filter.expression.EvaluationContext; +import org.apache.rocketmq.filter.expression.Expression; +import org.apache.rocketmq.filter.expression.MQFilterException; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class FilterSpiTest { + + static class NothingExpression implements Expression { + + @Override + public Object evaluate(final EvaluationContext context) throws Exception { + return Boolean.TRUE; + } + } + + static class NothingFilter implements FilterSpi { + @Override + public Expression compile(final String expr) throws MQFilterException { + return new NothingExpression(); + } + + @Override + public String ofType() { + return "Nothing"; + } + } + + @Test + public void testRegister() { + FilterFactory.INSTANCE.register(new NothingFilter()); + + Expression expr = null; + try { + expr = FilterFactory.INSTANCE.get("Nothing").compile("abc"); + } catch (MQFilterException e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + + assertThat(expr).isNotNull(); + + try { + assertThat((Boolean) expr.evaluate(new EmptyEvaluationContext())).isTrue(); + } catch (Exception e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + FilterFactory.INSTANCE.unRegister("Nothing"); + } + + @Test + public void testGet() { + try { + assertThat((Boolean) FilterFactory.INSTANCE.get(ExpressionType.SQL92).compile("a is not null and a > 0") + .evaluate(new EmptyEvaluationContext())).isFalse(); + } catch (Exception e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + } +} diff --git a/filter/src/test/java/org/apache/rocketmq/filter/ParserTest.java b/filter/src/test/java/org/apache/rocketmq/filter/ParserTest.java new file mode 100644 index 0000000..9e6291f --- /dev/null +++ b/filter/src/test/java/org/apache/rocketmq/filter/ParserTest.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.filter; + +import org.apache.rocketmq.filter.expression.Expression; +import org.apache.rocketmq.filter.expression.MQFilterException; +import org.apache.rocketmq.filter.parser.SelectorParser; +import org.junit.Test; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ParserTest { + + private static String andExpression = "a=3 and b<>4 And c>5 AND d<=4"; + private static String andExpressionHasBlank = "a=3 and b<>4 And c>5 AND d<=4"; + private static String orExpression = "a=3 or b<>4 Or c>5 OR d<=4"; + private static String inExpression = "a in ('3', '4', '5')"; + private static String notInExpression = "(a not in ('6', '4', '5')) or (b in ('3', '4', '5'))"; + private static String betweenExpression = "(a between 2 and 10) AND (b not between 6 and 9)"; + 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"; + + @Test + public void testParse_valid() { + for (String expr : Arrays.asList( + andExpression, orExpression, inExpression, notInExpression, betweenExpression, + equalNullExpression, notEqualNullExpression, nowExpression, containsExpression + )) { + + try { + Expression expression = SelectorParser.parse(expr); + assertThat(expression).isNotNull(); + } catch (MQFilterException e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + + } + } + + @Test + public void testParse_invalid() { + try { + SelectorParser.parse(invalidExpression); + + assertThat(Boolean.TRUE).isFalse(); + } catch (MQFilterException e) { + } + } + + @Test + public void testParse_decimalOverFlow() { + try { + String str = "100000000000000000000000"; + + SelectorParser.parse("a > " + str); + + assertThat(Boolean.TRUE).isFalse(); + } catch (Exception e) { + } + } + + @Test + public void testParse_floatOverFlow() { + try { + StringBuilder sb = new StringBuilder(210000); + sb.append("1"); + for (int i = 0; i < 2048; i ++) { + sb.append("111111111111111111111111111111111111111111111111111"); + } + sb.append("."); + for (int i = 0; i < 2048; i ++) { + sb.append("111111111111111111111111111111111111111111111111111"); + } + String str = sb.toString(); + + + SelectorParser.parse("a > " + str); + + assertThat(Boolean.TRUE).isFalse(); + } catch (Exception e) { + } + } + + @Test + public void testParse_illegalBetween() { + try { + SelectorParser.parse(illegalBetween); + + assertThat(Boolean.TRUE).isFalse(); + } catch (Exception e) { + } + } + + @Test + public void testEquals() { + try { + Expression expr1 = SelectorParser.parse(andExpression); + + Expression expr2 = SelectorParser.parse(andExpressionHasBlank); + + Expression expr3 = SelectorParser.parse(orExpression); + + assertThat(expr1).isEqualTo(expr2); + assertThat(expr1).isNotEqualTo(expr3); + } catch (MQFilterException e) { + e.printStackTrace(); + assertThat(Boolean.TRUE).isFalse(); + } + } +} 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 0000000..8695d52 --- /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/namesrv/BUILD.bazel b/namesrv/BUILD.bazel new file mode 100644 index 0000000..fec42ea --- /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 new file mode 100644 index 0000000..ad36af7 --- /dev/null +++ b/namesrv/pom.xml @@ -0,0 +1,68 @@ + + + + + org.apache.rocketmq + rocketmq-all + 5.3.4-SNAPSHOT + + + 4.0.0 + jar + rocketmq-namesrv + rocketmq-namesrv ${project.version} + + + ${basedir}/.. + + + + + ${project.groupId} + rocketmq-controller + + + ${project.groupId} + rocketmq-client + + + ${project.groupId} + rocketmq-tools + + + ${project.groupId} + rocketmq-srvutil + + + org.openjdk.jmh + jmh-core + 1.19 + test + + + org.openjdk.jmh + jmh-generator-annprocess + 1.19 + test + + + 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 new file mode 100644 index 0000000..be327cf --- /dev/null +++ b/namesrv/src/main/java/org/apache/rocketmq/namesrv/NamesrvController.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.namesrv; + +import java.util.Collections; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +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.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 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 KVConfigManager kvConfigManager; + private final RouteInfoManager routeInfoManager; + + private RemotingClient remotingClient; + private RemotingServer remotingServer; + + private final BrokerHousekeepingService brokerHousekeepingService; + + private ExecutorService defaultExecutor; + private ExecutorService clientRequestExecutor; + + private BlockingQueue defaultThreadPoolQueue; + private BlockingQueue clientRequestThreadPoolQueue; + + 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.brokerHousekeepingService = new BrokerHousekeepingService(this); + 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.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; + } + + String[] watchFiles = {TlsSystemConfig.tlsServerCertPath, TlsSystemConfig.tlsServerKeyPath, TlsSystemConfig.tlsServerTrustCertPath}; + + FileWatchService.Listener listener = new FileWatchService.Listener() { + boolean certChanged, keyChanged = false; + + @Override + 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(); + } + } + }; + + try { + fileWatchService = new FileWatchService(watchFiles, listener); + } catch (Exception e) { + LOGGER.warn("FileWatchService created error, can't load the certificate dynamically"); + } + } + + private void printWaterMark() { + WATER_MARK_LOG.info("[WATERMARK] ClientQueueSize:{} ClientQueueSlowTime:{} " + "DefaultQueueSize:{} DefaultQueueSlowTime:{}", this.clientRequestThreadPoolQueue.size(), headSlowTimeMills(this.clientRequestThreadPoolQueue), this.defaultThreadPoolQueue.size(), headSlowTimeMills(this.defaultThreadPoolQueue)); + } + + 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(); + } + } + + if (slowTimeMills < 0) { + slowTimeMills = 0; + } + + return slowTimeMills; + } + + private void registerProcessor() { + if (namesrvConfig.isClusterTest()) { + + 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.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.defaultExecutor.shutdown(); + this.clientRequestExecutor.shutdown(); + this.scheduledExecutorService.shutdown(); + this.scanExecutorService.shutdown(); + this.routeInfoManager.shutdown(); + + if (this.fileWatchService != null) { + this.fileWatchService.shutdown(); + } + } + + public NamesrvConfig getNamesrvConfig() { + return namesrvConfig; + } + + public NettyServerConfig getNettyServerConfig() { + return nettyServerConfig; + } + + public KVConfigManager getKvConfigManager() { + return kvConfigManager; + } + + public RouteInfoManager getRouteInfoManager() { + return routeInfoManager; + } + + public RemotingServer getRemotingServer() { + return remotingServer; + } + + public RemotingClient getRemotingClient() { + return remotingClient; + } + + public void setRemotingServer(RemotingServer remotingServer) { + this.remotingServer = remotingServer; + } + + public Configuration getConfiguration() { + return configuration; + } +} diff --git a/namesrv/src/main/java/org/apache/rocketmq/namesrv/NamesrvStartup.java b/namesrv/src/main/java/org/apache/rocketmq/namesrv/NamesrvStartup.java new file mode 100644 index 0000000..23d0906 --- /dev/null +++ b/namesrv/src/main/java/org/apache/rocketmq/namesrv/NamesrvStartup.java @@ -0,0 +1,243 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.io.BufferedInputStream; +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.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; + +public class NamesrvStartup { + + private static final Logger log = LoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME); + private static final 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) { + try { + parseCommandlineAndConfigFile(args); + NamesrvController controller = createAndStartNamesrvController(); + return controller; + } catch (Throwable e) { + e.printStackTrace(); + System.exit(-1); + } + + 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; + } + + 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(); + } + } + + 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); + } + + 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); + + } + + public static NamesrvController createAndStartNamesrvController() throws Exception { + + 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; + } + + public static NamesrvController createNamesrvController() { + + final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig, nettyClientConfig); + // remember all configs to prevent discard + controller.getConfiguration().registerConfig(properties); + return controller; + } + + public static NamesrvController start(final NamesrvController controller) throws Exception { + + if (null == controller) { + throw new IllegalArgumentException("NamesrvController 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 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"); + } + + 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) { + Option opt = new Option("c", "configFile", true, "Name server 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; + } + + 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 new file mode 100644 index 0000000..5c8a3d1 --- /dev/null +++ b/namesrv/src/main/java/org/apache/rocketmq/namesrv/kvconfig/KVConfigManager.java @@ -0,0 +1,194 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.kvconfig; + +import java.io.IOException; +import java.util.HashMap; +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.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.namesrv.NamesrvController; +import org.apache.rocketmq.remoting.protocol.body.KVTable; + +public class KVConfigManager { + private static final Logger log = LoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME); + + private final NamesrvController namesrvController; + + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + private final HashMap> configTable = + new HashMap<>(); + + public KVConfigManager(NamesrvController namesrvController) { + this.namesrvController = namesrvController; + } + + public void load() { + String content = null; + try { + content = MixAll.file2String(this.namesrvController.getNamesrvConfig().getKvConfigPath()); + } catch (IOException e) { + log.warn("Load KV config table exception", e); + } + if (content != null) { + KVConfigSerializeWrapper kvConfigSerializeWrapper = + KVConfigSerializeWrapper.fromJson(content, KVConfigSerializeWrapper.class); + if (null != kvConfigSerializeWrapper) { + this.configTable.putAll(kvConfigSerializeWrapper.getConfigTable()); + log.info("load KV config table OK"); + } + } + } + + public void putKVConfig(final String namespace, final String key, final String value) { + try { + this.lock.writeLock().lockInterruptibly(); + try { + HashMap kvTable = this.configTable.get(namespace); + if (null == kvTable) { + kvTable = new HashMap<>(); + this.configTable.put(namespace, kvTable); + log.info("putKVConfig create new Namespace {}", namespace); + } + + final String prev = kvTable.put(key, value); + if (null != prev) { + log.info("putKVConfig update config item, Namespace: {} Key: {} Value: {}", + namespace, key, value); + } else { + log.info("putKVConfig create new config item, Namespace: {} Key: {} Value: {}", + namespace, key, value); + } + } finally { + this.lock.writeLock().unlock(); + } + } catch (InterruptedException e) { + log.error("putKVConfig InterruptedException", e); + } + + this.persist(); + } + + public void persist() { + try { + this.lock.readLock().lockInterruptibly(); + try { + KVConfigSerializeWrapper kvConfigSerializeWrapper = new KVConfigSerializeWrapper(); + kvConfigSerializeWrapper.setConfigTable(this.configTable); + + String content = kvConfigSerializeWrapper.toJson(); + + if (null != content) { + MixAll.string2File(content, this.namesrvController.getNamesrvConfig().getKvConfigPath()); + } + } catch (IOException e) { + log.error("persist kvconfig Exception, " + + this.namesrvController.getNamesrvConfig().getKvConfigPath(), e); + } finally { + this.lock.readLock().unlock(); + } + } catch (InterruptedException e) { + log.error("persist InterruptedException", e); + } + + } + + public void deleteKVConfig(final String namespace, final String key) { + try { + this.lock.writeLock().lockInterruptibly(); + try { + HashMap kvTable = this.configTable.get(namespace); + if (null != kvTable) { + String value = kvTable.remove(key); + log.info("deleteKVConfig delete a config item, Namespace: {} Key: {} Value: {}", + namespace, key, value); + } + } finally { + this.lock.writeLock().unlock(); + } + } catch (InterruptedException e) { + log.error("deleteKVConfig InterruptedException", e); + } + + this.persist(); + } + + public byte[] getKVListByNamespace(final String namespace) { + try { + this.lock.readLock().lockInterruptibly(); + try { + HashMap kvTable = this.configTable.get(namespace); + if (null != kvTable) { + KVTable table = new KVTable(); + table.setTable(kvTable); + return table.encode(); + } + } finally { + this.lock.readLock().unlock(); + } + } catch (InterruptedException e) { + log.error("getKVListByNamespace InterruptedException", e); + } + + return null; + } + + public String getKVConfig(final String namespace, final String key) { + try { + this.lock.readLock().lockInterruptibly(); + try { + HashMap kvTable = this.configTable.get(namespace); + if (null != kvTable) { + return kvTable.get(key); + } + } finally { + this.lock.readLock().unlock(); + } + } catch (InterruptedException e) { + log.error("getKVConfig InterruptedException", e); + } + + return null; + } + + public void printAllPeriodically() { + try { + this.lock.readLock().lockInterruptibly(); + try { + log.info("--------------------------------------------------------"); + + { + log.info("configTable SIZE: {}", this.configTable.size()); + for (Entry> next : this.configTable.entrySet()) { + for (Entry nextSub : next.getValue().entrySet()) { + log.info("configTable NS: {} Key: {} Value: {}", next.getKey(), nextSub.getKey(), + nextSub.getValue()); + } + } + } + } finally { + this.lock.readLock().unlock(); + } + } catch (InterruptedException e) { + log.error("printAllPeriodically InterruptedException", e); + } + } +} diff --git a/namesrv/src/main/java/org/apache/rocketmq/namesrv/kvconfig/KVConfigSerializeWrapper.java b/namesrv/src/main/java/org/apache/rocketmq/namesrv/kvconfig/KVConfigSerializeWrapper.java new file mode 100644 index 0000000..3b53e19 --- /dev/null +++ b/namesrv/src/main/java/org/apache/rocketmq/namesrv/kvconfig/KVConfigSerializeWrapper.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.namesrv.kvconfig; + +import java.util.HashMap; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class KVConfigSerializeWrapper extends RemotingSerializable { + private HashMap> configTable; + + public HashMap> getConfigTable() { + return configTable; + } + + public void setConfigTable(HashMap> configTable) { + this.configTable = configTable; + } +} 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 0000000..1ef6bea --- /dev/null +++ b/namesrv/src/main/java/org/apache/rocketmq/namesrv/processor/ClientRequestProcessor.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.namesrv.processor; + +import com.alibaba.fastjson.serializer.SerializerFeature; +import io.netty.channel.ChannelHandlerContext; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +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; + + 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 = 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) { + 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 new file mode 100644 index 0000000..725c5e6 --- /dev/null +++ b/namesrv/src/main/java/org/apache/rocketmq/namesrv/processor/ClusterTestRequestProcessor.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.namesrv.processor; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.client.exception.MQClientException; +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.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; + +public class ClusterTestRequestProcessor extends ClientRequestProcessor { + private static final Logger log = LoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME); + private final DefaultMQAdminExt adminExt; + private final String productEnvName; + + public ClusterTestRequestProcessor(NamesrvController namesrvController, String productEnvName) { + super(namesrvController); + this.productEnvName = productEnvName; + adminExt = new DefaultMQAdminExt(); + adminExt.setInstanceName("CLUSTER_TEST_NS_INS_" + productEnvName); + adminExt.setUnitName(productEnvName); + try { + adminExt.start(); + } catch (MQClientException e) { + log.error("Failed to start processor", e); + } + } + + @Override + public RemotingCommand getRouteInfoByTopic(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) { + String orderTopicConf = + this.namesrvController.getKvConfigManager().getKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG, + requestHeader.getTopic()); + topicRouteData.setOrderTopicConf(orderTopicConf); + } else { + try { + topicRouteData = adminExt.examineTopicRouteInfo(requestHeader.getTopic()); + } catch (Exception e) { + log.info("get route info by topic from product environment failed. envName={},", productEnvName); + } + } + + if (topicRouteData != null) { + byte[] 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; + } +} 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 new file mode 100644 index 0000000..deb51e4 --- /dev/null +++ b/namesrv/src/main/java/org/apache/rocketmq/namesrv/processor/DefaultRequestProcessor.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.namesrv.processor; + +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.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.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.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 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 (ctx != null) { + log.debug("receive request, {} {} {}", + request.getCode(), + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), + request); + } + + switch (request.getCode()) { + case RequestCode.PUT_KV_CONFIG: + return this.putKVConfig(ctx, request); + case RequestCode.GET_KV_CONFIG: + 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: + return this.registerBroker(ctx, request); + case RequestCode.UNREGISTER_BROKER: + return this.unregisterBroker(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 this.getAllTopicListFromNameserver(ctx, request); + case RequestCode.DELETE_TOPIC_IN_NAMESRV: + 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: + return this.getTopicsByCluster(ctx, request); + case RequestCode.GET_SYSTEM_TOPIC_LIST_FROM_NS: + return this.getSystemTopicListFromNs(ctx, request); + case RequestCode.GET_UNIT_TOPIC_LIST: + return this.getUnitTopicList(ctx, request); + case RequestCode.GET_HAS_UNIT_SUB_TOPIC_LIST: + return this.getHasUnitSubTopicList(ctx, request); + case RequestCode.GET_HAS_UNIT_SUB_UNUNIT_TOPIC_LIST: + return this.getHasUnitSubUnUnitTopicList(ctx, request); + case RequestCode.UPDATE_NAMESRV_CONFIG: + return this.updateConfig(ctx, request); + case RequestCode.GET_NAMESRV_CONFIG: + return this.getConfig(ctx, request); + default: + String error = " request type " + request.getCode() + " not supported"; + return RemotingCommand.createResponseCommand(RemotingSysResponseCode.REQUEST_CODE_NOT_SUPPORTED, error); + } + } + + @Override + public boolean rejectRequest() { + return false; + } + + public RemotingCommand putKVConfig(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + 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(), + requestHeader.getValue() + ); + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + public RemotingCommand getKVConfig(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(GetKVConfigResponseHeader.class); + final GetKVConfigResponseHeader responseHeader = (GetKVConfigResponseHeader) response.readCustomHeader(); + final GetKVConfigRequestHeader requestHeader = + (GetKVConfigRequestHeader) request.decodeCommandCustomHeader(GetKVConfigRequestHeader.class); + + String value = this.namesrvController.getKvConfigManager().getKVConfig( + requestHeader.getNamespace(), + requestHeader.getKey() + ); + + if (value != null) { + responseHeader.setValue(value); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + response.setCode(ResponseCode.QUERY_NOT_FOUND); + response.setRemark("No config item, Namespace: " + requestHeader.getNamespace() + " Key: " + requestHeader.getKey()); + return response; + } + + public RemotingCommand deleteKVConfig(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + final DeleteKVConfigRequestHeader requestHeader = + (DeleteKVConfigRequestHeader) request.decodeCommandCustomHeader(DeleteKVConfigRequestHeader.class); + + this.namesrvController.getKvConfigManager().deleteKVConfig( + requestHeader.getNamespace(), + requestHeader.getKey() + ); + + 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); + + if (!checksum(ctx, request, requestHeader)) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("crc32 not match"); + return response; + } + + 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 of old version only contains TopicConfig. + topicConfigWrapper = extractRegisterTopicConfigFromRequest(request); + } + + RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker( + requestHeader.getClusterName(), + requestHeader.getBrokerAddr(), + requestHeader.getBrokerName(), + requestHeader.getBrokerId(), + requestHeader.getHaServerAddr(), + 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()); + + 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; + } + + 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(0L); + topicConfigWrapper.getDataVersion().setStateVersion(0L); + } + return topicConfigWrapper; + } + + private RegisterBrokerBody extractRegisterBrokerBodyFromRequest(RemotingCommand request, + RegisterBrokerRequestHeader requestHeader) throws RemotingCommandException { + RegisterBrokerBody registerBrokerBody = new RegisterBrokerBody(); + + 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; + } + + 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); + + 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 brokerHeartbeat(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + final BrokerHeartbeatRequestHeader requestHeader = + (BrokerHeartbeatRequestHeader) request.decodeCommandCustomHeader(BrokerHeartbeatRequestHeader.class); + + this.namesrvController.getRouteInfoManager().updateBrokerInfoUpdateTimestamp(requestHeader.getClusterName(), requestHeader.getBrokerAddr()); + + 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().encode(); + response.setBody(content); + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private RemotingCommand wipeWritePermOfBroker(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(WipeWritePermOfBrokerResponseHeader.class); + final WipeWritePermOfBrokerResponseHeader responseHeader = (WipeWritePermOfBrokerResponseHeader) response.readCustomHeader(); + final WipeWritePermOfBrokerRequestHeader requestHeader = + (WipeWritePermOfBrokerRequestHeader) request.decodeCommandCustomHeader(WipeWritePermOfBrokerRequestHeader.class); + + int wipeTopicCnt = this.namesrvController.getRouteInfoManager().wipeWritePermOfBrokerByLock(requestHeader.getBrokerName()); + + 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()), + addTopicCnt); + + responseHeader.setAddTopicCount(addTopicCnt); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + 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; + } + + 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.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private RemotingCommand deleteTopicInNamesrv(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + final DeleteTopicFromNamesrvRequestHeader requestHeader = + (DeleteTopicFromNamesrvRequestHeader) request.decodeCommandCustomHeader(DeleteTopicFromNamesrvRequestHeader.class); + + 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); + return response; + } + + private RemotingCommand getKVListByNamespace(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + final GetKVListByNamespaceRequestHeader requestHeader = + (GetKVListByNamespaceRequestHeader) request.decodeCommandCustomHeader(GetKVListByNamespaceRequestHeader.class); + + byte[] jsonValue = this.namesrvController.getKvConfigManager().getKVListByNamespace( + requestHeader.getNamespace()); + if (null != jsonValue) { + response.setBody(jsonValue); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + response.setCode(ResponseCode.QUERY_NOT_FOUND); + response.setRemark("No config item, Namespace: " + requestHeader.getNamespace()); + return response; + } + + 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); + + 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) { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + TopicList systemTopicList = this.namesrvController.getRouteInfoManager().getSystemTopicList(); + byte[] body = systemTopicList.encode(); + + response.setBody(body); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private RemotingCommand getUnitTopicList(ChannelHandlerContext ctx, + RemotingCommand request) { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + 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"); + } + + return response; + } + + private RemotingCommand getHasUnitSubTopicList(ChannelHandlerContext ctx, + RemotingCommand request) { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + 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"); + } + + return response; + } + + private RemotingCommand getHasUnitSubUnUnitTopicList(ChannelHandlerContext ctx, RemotingCommand request) { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + 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"); + } + + return response; + } + + private RemotingCommand updateConfig(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.namesrvController.getConfiguration().update(properties); + } + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private RemotingCommand getConfig(ChannelHandlerContext ctx, RemotingCommand request) { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + String content = this.namesrvController.getConfiguration().getAllConfigsFormatString(); + if (StringUtils.isNotBlank(content)) { + 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; + } + + 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 0000000..a740a0f --- /dev/null +++ b/namesrv/src/main/java/org/apache/rocketmq/namesrv/route/ZoneRouteRPCHook.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.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()) { + if (brokerData.getBrokerAddrs() == null) { + continue; + } + //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(); + 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 0000000..02cc722 --- /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 new file mode 100644 index 0000000..b527429 --- /dev/null +++ b/namesrv/src/main/java/org/apache/rocketmq/namesrv/routeinfo/BrokerHousekeepingService.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.namesrv.routeinfo; + +import io.netty.channel.Channel; +import org.apache.rocketmq.namesrv.NamesrvController; +import org.apache.rocketmq.remoting.ChannelEventListener; + +public class BrokerHousekeepingService implements ChannelEventListener { + + private final NamesrvController namesrvController; + + public BrokerHousekeepingService(NamesrvController namesrvController) { + this.namesrvController = namesrvController; + } + + @Override + public void onChannelConnect(String remoteAddr, Channel channel) { + } + + @Override + public void onChannelClose(String remoteAddr, Channel channel) { + this.namesrvController.getRouteInfoManager().onChannelDestroy(channel); + } + + @Override + public void onChannelException(String remoteAddr, Channel channel) { + this.namesrvController.getRouteInfoManager().onChannelDestroy(channel); + } + + @Override + public void onChannelIdle(String remoteAddr, Channel 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 new file mode 100644 index 0000000..e3f9d0d --- /dev/null +++ b/namesrv/src/main/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManager.java @@ -0,0 +1,1278 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.util.ArrayList; +import java.util.Collections; +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.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.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.NamesrvConfig; +import org.apache.rocketmq.common.sysflag.TopicSysFlag; +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 static final long DEFAULT_BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2; + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + 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 void shutdown() { + this.unRegisterService.shutdown(true); + } + + 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; + } + + 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 { + 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 TopicList getAllTopicList() { + TopicList topicList = new TopicList(); + try { + 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; + } + + 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 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 { + this.lock.writeLock().lockInterruptibly(); + + //init or update the cluster info + Set brokerNames = ConcurrentHashMapUtils.computeIfAbsent((ConcurrentHashMap>) this.clusterAddrTable, clusterName, k -> new HashSet<>()); + brokerNames.add(brokerName); + + 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); + } + + 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("Registering 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; + } + } + } + + 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); + } + } + } + + 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 (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()); + } + } + } + } + + 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); + } + } + } + + 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 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; + } + + // 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) { + QueueData queueData = new QueueData(); + queueData.setBrokerName(brokerName); + queueData.setWriteQueueNums(topicConfig.getWriteQueueNums()); + queueData.setReadQueueNums(topicConfig.getReadQueueNums()); + queueData.setPerm(topicConfig.getPerm()); + queueData.setTopicSysFlag(topicConfig.getTopicSysFlag()); + + 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 { + 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); + } + } + } + + public int wipeWritePermOfBrokerByLock(final String brokerName) { + try { + try { + this.lock.writeLock().lockInterruptibly(); + return operateWritePermOfBroker(brokerName, RequestCode.WIPE_WRITE_PERM_OF_BROKER); + } finally { + this.lock.writeLock().unlock(); + } + } catch (Exception e) { + log.error("wipeWritePermOfBrokerByLock Exception", e); + } + + return 0; + } + + 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; + + 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; + break; + case RequestCode.ADD_WRITE_PERM_OF_BROKER: + perm = PermName.PERM_READ | PermName.PERM_WRITE; + break; + } + qd.setPerm(perm); + topicCnt++; + } + return topicCnt; + } + + public void unregisterBroker( + final String clusterName, + 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 { + 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", + brokerAddrInfo + ); + + this.filterServerTable.remove(brokerAddrInfo); + + boolean removeBrokerName = false; + boolean isMinBrokerIdChanged = false; + BrokerData brokerData = this.brokerAddrTable.get(brokerName); + if (null != brokerData) { + 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 {}, {}", + removed ? "OK" : "Failed", + brokerAddrInfo + ); + if (brokerData.getBrokerAddrs().isEmpty()) { + this.brokerAddrTable.remove(brokerName); + log.info("unregisterBroker, remove name from brokerAddrTable OK, {}", + brokerName + ); + + removeBrokerName = true; + } else if (isMinBrokerIdChanged) { + needNotifyBrokerMap.put(brokerName, new BrokerStatusChangeInfo( + brokerData.getBrokerAddrs(), brokerAddr, null)); + } + } + + if (removeBrokerName) { + Set nameSet = this.clusterAddrTable.get(clusterName); + if (nameSet != null) { + boolean removed = nameSet.remove(brokerName); + log.info("unregisterBroker, remove name from clusterAddrTable {}, {}", + removed ? "OK" : "Failed", + brokerName); + + if (nameSet.isEmpty()) { + this.clusterAddrTable.remove(clusterName); + log.info("unregisterBroker, remove cluster from clusterAddrTable {}", + clusterName + ); + } + } + removedBroker.add(brokerName); + } else { + reducedBroker.add(brokerName); + } + } + + 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 cleanTopicByUnRegisterRequests(Set removedBroker, Set reducedBroker) { + Iterator>> itMap = this.topicQueueTable.entrySet().iterator(); + while (itMap.hasNext()) { + Entry> entry = itMap.next(); + + String topic = entry.getKey(); + 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 (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; + List brokerDataList = new LinkedList<>(); + topicRouteData.setBrokerDatas(brokerDataList); + + HashMap> filterServerMap = new HashMap<>(); + topicRouteData.setFilterServerTable(filterServerMap); + + try { + 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); + + 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); + } + + } + } + } catch (Exception e) { + log.error("pickupTopicRouteData Exception", e); + } finally { + this.lock.readLock().unlock(); + } + + 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; + } + + return null; + } + + public void scanNotActiveBroker() { + 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(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(); + 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(); + } + } 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); + } + } + + private boolean setupUnRegisterRequest(UnRegisterBrokerRequestHeader unRegisterRequest, + BrokerAddrInfo brokerAddrInfo) { + unRegisterRequest.setClusterName(brokerAddrInfo.getClusterName()); + unRegisterRequest.setBrokerAddr(brokerAddrInfo.getBrokerAddr()); + + for (Entry stringBrokerDataEntry : this.brokerAddrTable.entrySet()) { + BrokerData brokerData = stringBrokerDataEntry.getValue(); + if (!brokerAddrInfo.getClusterName().equals(brokerData.getCluster())) { + continue; + } + + 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; + } + } + } + + return false; + } + + 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()); + } + } + } + + 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 { + this.lock.readLock().lockInterruptibly(); + log.info("--------------------------------------------------------"); + { + log.info("topicQueueTable SIZE: {}", this.topicQueueTable.size()); + for (Entry> next : this.topicQueueTable.entrySet()) { + log.info("topicQueueTable Topic: {} {}", next.getKey(), next.getValue()); + } + } + + { + log.info("brokerAddrTable SIZE: {}", this.brokerAddrTable.size()); + for (Entry next : this.brokerAddrTable.entrySet()) { + log.info("brokerAddrTable brokerName: {} {}", next.getKey(), next.getValue()); + } + } + + { + log.info("brokerLiveTable SIZE: {}", this.brokerLiveTable.size()); + for (Entry next : this.brokerLiveTable.entrySet()) { + log.info("brokerLiveTable brokerAddr: {} {}", next.getKey(), next.getValue()); + } + } + + { + log.info("clusterAddrTable SIZE: {}", this.clusterAddrTable.size()); + for (Entry> next : this.clusterAddrTable.entrySet()) { + log.info("clusterAddrTable clusterName: {} {}", next.getKey(), next.getValue()); + } + } + } finally { + this.lock.readLock().unlock(); + } + } catch (Exception e) { + log.error("printAllPeriodically Exception", e); + } + } + + public TopicList getSystemTopicList() { + TopicList topicList = new TopicList(); + try { + this.lock.readLock().lockInterruptibly(); + for (Map.Entry> entry : clusterAddrTable.entrySet()) { + topicList.getTopicList().add(entry.getKey()); + topicList.getTopicList().addAll(entry.getValue()); + } + + 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; + } + } + } + } catch (Exception e) { + log.error("getSystemTopicList Exception", e); + } finally { + this.lock.readLock().unlock(); + } + + return topicList; + } + + 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) { + for (Entry> topicEntry : this.topicQueueTable.entrySet()) { + String topic = topicEntry.getKey(); + Map queueDataMap = topicEntry.getValue(); + final QueueData qd = queueDataMap.get(brokerName); + if (qd != null) { + topicList.getTopicList().add(topic); + } + } + } + } finally { + this.lock.readLock().unlock(); + } + } catch (Exception e) { + log.error("getTopicsByCluster Exception", e); + } + + return topicList; + } + + public TopicList getUnitTopics() { + TopicList topicList = new TopicList(); + try { + 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); + } + } + } catch (Exception e) { + log.error("getUnitTopics Exception", e); + } finally { + this.lock.readLock().unlock(); + } + + return topicList; + } + + public TopicList getHasUnitSubTopicList() { + TopicList topicList = new TopicList(); + try { + 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); + } + } + } catch (Exception e) { + log.error("getHasUnitSubTopicList Exception", e); + } finally { + this.lock.readLock().unlock(); + } + + return topicList; + } + + public TopicList getHasUnitSubUnUnitTopicList() { + TopicList topicList = new TopicList(); + try { + 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); + } + } + } catch (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; + } + + 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, long heartbeatTimeoutMillis, DataVersion dataVersion, + Channel channel, + String haServerAddr) { + this.lastUpdateTimestamp = lastUpdateTimestamp; + this.heartbeatTimeoutMillis = heartbeatTimeoutMillis; + this.dataVersion = dataVersion; + this.channel = channel; + this.haServerAddr = haServerAddr; + } + + public long getLastUpdateTimestamp() { + return lastUpdateTimestamp; + } + + 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; + } + + public void setDataVersion(DataVersion dataVersion) { + this.dataVersion = dataVersion; + } + + public Channel getChannel() { + return channel; + } + + public void setChannel(Channel channel) { + this.channel = channel; + } + + public String getHaServerAddr() { + return haServerAddr; + } + + public void setHaServerAddr(String haServerAddr) { + this.haServerAddr = haServerAddr; + } + + @Override + public String toString() { + return "BrokerLiveInfo [lastUpdateTimestamp=" + lastUpdateTimestamp + ", dataVersion=" + dataVersion + + ", 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 0000000..2a3c957 --- /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 new file mode 100644 index 0000000..7768272 --- /dev/null +++ b/namesrv/src/test/java/org/apache/rocketmq/namesrv/NameServerInstanceTest.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.namesrv; + +import org.apache.rocketmq.common.namesrv.NamesrvConfig; +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; + +public class NameServerInstanceTest { + protected NamesrvController nameSrvController = null; + protected NettyServerConfig nettyServerConfig = new NettyServerConfig(); + protected NamesrvConfig namesrvConfig = new NamesrvConfig(); + + @Before + public void startup() throws Exception { + nettyServerConfig.setListenPort(9876); + nameSrvController = new NamesrvController(namesrvConfig, nettyServerConfig); + boolean initResult = nameSrvController.initialize(); + assertThat(initResult).isTrue(); + nameSrvController.start(); + } + + /** + * Add a placeholder to make Bazel happy. + */ + @Test + public void itWorks() { + + } + + @After + public void shutdown() throws Exception { + if (nameSrvController != null) { + nameSrvController.shutdown(); + } + //maybe need to clean the file store. But we do not suggest deleting anything. + } +} 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 0000000..49d7103 --- /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 0000000..957406d --- /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/kvconfig/KVConfigManagerTest.java b/namesrv/src/test/java/org/apache/rocketmq/namesrv/kvconfig/KVConfigManagerTest.java new file mode 100644 index 0000000..b74d66b --- /dev/null +++ b/namesrv/src/test/java/org/apache/rocketmq/namesrv/kvconfig/KVConfigManagerTest.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.namesrv.kvconfig; + +import org.apache.rocketmq.common.namesrv.NamesrvUtil; +import org.apache.rocketmq.namesrv.NameServerInstanceTest; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class KVConfigManagerTest extends NameServerInstanceTest { + private KVConfigManager kvConfigManager; + + @Before + public void setup() throws Exception { + kvConfigManager = new KVConfigManager(nameSrvController); + } + + @Test + public void testPutKVConfig() { + kvConfigManager.putKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG, "UnitTest", "test"); + byte[] kvConfig = kvConfigManager.getKVListByNamespace(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG); + assertThat(kvConfig).isNotNull(); + String value = kvConfigManager.getKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG, "UnitTest"); + assertThat(value).isEqualTo("test"); + } + + @Test + public void testDeleteKVConfig() { + kvConfigManager.deleteKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG, "UnitTest"); + byte[] kvConfig = kvConfigManager.getKVListByNamespace(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG); + assertThat(kvConfig).isNull(); + Assert.assertTrue(kvConfig == null); + String value = kvConfigManager.getKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG, "UnitTest"); + assertThat(value).isNull(); + } +} \ No newline at end of file diff --git a/namesrv/src/test/java/org/apache/rocketmq/namesrv/kvconfig/KVConfigSerializeWrapperTest.java b/namesrv/src/test/java/org/apache/rocketmq/namesrv/kvconfig/KVConfigSerializeWrapperTest.java new file mode 100644 index 0000000..a24d86d --- /dev/null +++ b/namesrv/src/test/java/org/apache/rocketmq/namesrv/kvconfig/KVConfigSerializeWrapperTest.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.namesrv.kvconfig; + +import java.util.HashMap; +import org.apache.rocketmq.common.namesrv.NamesrvUtil; +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class KVConfigSerializeWrapperTest { + private KVConfigSerializeWrapper kvConfigSerializeWrapper; + + @Before + public void setup() throws Exception { + kvConfigSerializeWrapper = new KVConfigSerializeWrapper(); + } + + @Test + public void testEncodeAndDecode() { + HashMap> result = new HashMap<>(); + HashMap kvs = new HashMap<>(); + kvs.put("broker-name", "default-broker"); + kvs.put("topic-name", "default-topic"); + kvs.put("cid", "default-consumer-name"); + result.put(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG, kvs); + kvConfigSerializeWrapper.setConfigTable(result); + byte[] serializeByte = KVConfigSerializeWrapper.encode(kvConfigSerializeWrapper); + assertThat(serializeByte).isNotNull(); + + KVConfigSerializeWrapper deserializeObject = KVConfigSerializeWrapper.decode(serializeByte, KVConfigSerializeWrapper.class); + assertThat(deserializeObject.getConfigTable()).containsKey(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG); + assertThat(deserializeObject.getConfigTable().get(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG).get("broker-name")).isEqualTo("default-broker"); + assertThat(deserializeObject.getConfigTable().get(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG).get("topic-name")).isEqualTo("default-topic"); + assertThat(deserializeObject.getConfigTable().get(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG).get("cid")).isEqualTo("default-consumer-name"); + } + +} \ 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 new file mode 100644 index 0000000..283f903 --- /dev/null +++ b/namesrv/src/test/java/org/apache/rocketmq/namesrv/processor/ClusterTestRequestProcessorTest.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.namesrv.processor; + +import io.netty.channel.ChannelHandlerContext; +import java.lang.reflect.Field; +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.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; +import org.junit.Before; +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; +import static org.mockito.Mockito.when; + +public class ClusterTestRequestProcessorTest { + private ClusterTestRequestProcessor clusterTestProcessor; + private DefaultMQAdminExtImpl defaultMQAdminExtImpl; + private MQClientInstance mqClientInstance = MQClientManager.getInstance() + .getOrCreateMQClientInstance(new ClientConfig()); + private MQClientAPIImpl mQClientAPIImpl; + private ChannelHandlerContext ctx; + + @Before + public void init() throws NoSuchFieldException, IllegalAccessException, RemotingException, MQClientException, + InterruptedException { + NamesrvController namesrvController = new NamesrvController( + new NamesrvConfig(), + new NettyServerConfig()); + + clusterTestProcessor = new ClusterTestRequestProcessor(namesrvController, "default-producer"); + mQClientAPIImpl = mock(MQClientAPIImpl.class); + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(); + defaultMQAdminExtImpl = new DefaultMQAdminExtImpl(defaultMQAdminExt, 1000); + ctx = mock(ChannelHandlerContext.class); + + Field field = DefaultMQAdminExtImpl.class.getDeclaredField("mqClientInstance"); + field.setAccessible(true); + field.set(defaultMQAdminExtImpl, mqClientInstance); + field = MQClientInstance.class.getDeclaredField("mQClientAPIImpl"); + field.setAccessible(true); + field.set(mqClientInstance, mQClientAPIImpl); + field = ClusterTestRequestProcessor.class.getDeclaredField("adminExt"); + field.setAccessible(true); + field.set(clusterTestProcessor, defaultMQAdminExt); + + TopicRouteData topicRouteData = new TopicRouteData(); + List brokerDatas = new ArrayList<>(); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(1234L, "127.0.0.1:10911"); + BrokerData brokerData = new BrokerData(); + brokerData.setCluster("default-cluster"); + brokerData.setBrokerName("default-broker"); + brokerData.setBrokerAddrs(brokerAddrs); + brokerDatas.add(brokerData); + topicRouteData.setBrokerDatas(brokerDatas); + when(mQClientAPIImpl.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(topicRouteData); + } + + @After + public void terminate() { + } + + @Test + public void testGetRouteInfoByTopic() throws RemotingCommandException { + RemotingCommand request = RemotingCommand.createRequestCommand(12, new CommandCustomHeader() { + @Override + public void checkFields() throws RemotingCommandException { + + } + }); + RemotingCommand remoting = clusterTestProcessor.getRouteInfoByTopic(ctx, request); + assertThat(remoting.getCode()).isEqualTo(ResponseCode.TOPIC_NOT_EXIST); + assertThat(remoting.getBody()).isNull(); + assertThat(remoting.getRemark()).isNotNull(); + } + + @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/RequestProcessorTest.java b/namesrv/src/test/java/org/apache/rocketmq/namesrv/processor/RequestProcessorTest.java new file mode 100644 index 0000000..831558a --- /dev/null +++ b/namesrv/src/test/java/org/apache/rocketmq/namesrv/processor/RequestProcessorTest.java @@ -0,0 +1,666 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 testQueryDataVersion()throws RemotingCommandException { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(null); + RemotingCommand request = getRemotingCommand(RequestCode.QUERY_DATA_VERSION); + RemotingCommand remotingCommand = defaultRequestProcessor.processRequest(ctx, request); + assertThat(remotingCommand.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetBrokerMemberBroker() throws RemotingCommandException { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(null); + RemotingCommand request = getRemotingCommand(RequestCode.GET_BROKER_MEMBER_GROUP); + RemotingCommand remotingCommand = defaultRequestProcessor.processRequest(ctx, request); + assertThat(remotingCommand.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testBrokerHeartBeat() throws RemotingCommandException { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(null); + RemotingCommand request = getRemotingCommand(RequestCode.BROKER_HEARTBEAT); + RemotingCommand remotingCommand = defaultRequestProcessor.processRequest(ctx, request); + assertThat(remotingCommand.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testAddWritePermOfBroker() throws RemotingCommandException { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(null); + RemotingCommand request = getRemotingCommand(RequestCode.ADD_WRITE_PERM_OF_BROKER); + 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/route/ZoneRouteRPCHookMoreTest.java b/namesrv/src/test/java/org/apache/rocketmq/namesrv/route/ZoneRouteRPCHookMoreTest.java new file mode 100644 index 0000000..ed42bec --- /dev/null +++ b/namesrv/src/test/java/org/apache/rocketmq/namesrv/route/ZoneRouteRPCHookMoreTest.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.namesrv.route; + + +import org.apache.rocketmq.common.MixAll; +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; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class ZoneRouteRPCHookMoreTest { + + private ZoneRouteRPCHook zoneRouteRPCHook; + + @Before + public void setUp() { + zoneRouteRPCHook = new ZoneRouteRPCHook(); + } + + @Test + public void testFilterByZoneName_ValidInput_ShouldFilterCorrectly() { + // Arrange + TopicRouteData topicRouteData = new TopicRouteData(); + topicRouteData.setBrokerDatas(generateBrokerDataList()); + topicRouteData.setQueueDatas(generateQueueDataList()); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ROUTEINFO_BY_TOPIC,null); + request.setExtFields(createExtFields("true","ZoneA")); + + RemotingCommand response = RemotingCommand.createResponseCommand(ResponseCode.SUCCESS, "remark"); + + // Act + zoneRouteRPCHook.doAfterResponse("127.0.0.1", request, response); + TopicRouteData decodedResponse = RemotingSerializable.decode(response.getBody(), TopicRouteData.class); + + // Assert + assertNull(decodedResponse); + } + + @Test + public void testFilterByZoneName_NoZoneName_ShouldNotFilter() { + // Arrange + TopicRouteData topicRouteData = new TopicRouteData(); + topicRouteData.setBrokerDatas(generateBrokerDataList()); + topicRouteData.setQueueDatas(generateQueueDataList()); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ROUTEINFO_BY_TOPIC,null); + HashMap extFields = new HashMap<>(); + extFields.put(MixAll.ZONE_MODE, "true"); + request.setExtFields(extFields); + + RemotingCommand response = RemotingCommand.createResponseCommand(ResponseCode.SUCCESS, null); + + // Act + zoneRouteRPCHook.doAfterResponse("127.0.0.1", request, response); + TopicRouteData decodedResponse = RemotingSerializable.decode(response.getBody(), TopicRouteData.class); + + // Assert + assertEquals(topicRouteData.getBrokerDatas().size(), 2); + assertEquals(topicRouteData.getQueueDatas().size(), 2); + } + + @Test + public void testFilterByZoneName_ZoneModeFalse_ShouldNotFilter() { + // Arrange + TopicRouteData topicRouteData = new TopicRouteData(); + topicRouteData.setBrokerDatas(generateBrokerDataList()); + topicRouteData.setQueueDatas(generateQueueDataList()); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ROUTEINFO_BY_TOPIC,null); + request.setExtFields(createExtFields("false","ZoneA")); + + RemotingCommand response = RemotingCommand.createResponseCommand(ResponseCode.SUCCESS ,null); + + // Act + zoneRouteRPCHook.doAfterResponse("127.0.0.1", request, response); + TopicRouteData decodedResponse = RemotingSerializable.decode(response.getBody(), TopicRouteData.class); + + // Assert + assertEquals(topicRouteData.getBrokerDatas().size(), 2); + assertEquals(topicRouteData.getQueueDatas().size(), 2); + } + + private List generateBrokerDataList() { + List brokerDataList = new ArrayList<>(); + BrokerData brokerData1 = new BrokerData(); + brokerData1.setBrokerName("BrokerA"); + brokerData1.setZoneName("ZoneA"); + Map brokerAddrs = new HashMap<>(); + brokerAddrs.put(MixAll.MASTER_ID, "127.0.0.1:10911"); + brokerData1.setBrokerAddrs((HashMap) brokerAddrs); + brokerDataList.add(brokerData1); + + BrokerData brokerData2 = new BrokerData(); + brokerData2.setBrokerName("BrokerB"); + brokerData2.setZoneName("ZoneB"); + brokerAddrs = new HashMap<>(); + brokerAddrs.put(MixAll.MASTER_ID, "127.0.0.1:10912"); + brokerData2.setBrokerAddrs((HashMap) brokerAddrs); + brokerDataList.add(brokerData2); + + return brokerDataList; + } + + private List generateQueueDataList() { + List queueDataList = new ArrayList<>(); + QueueData queueData1 = new QueueData(); + queueData1.setBrokerName("BrokerA"); + queueDataList.add(queueData1); + + QueueData queueData2 = new QueueData(); + queueData2.setBrokerName("BrokerB"); + queueDataList.add(queueData2); + + return queueDataList; + } + + private HashMap createExtFields(String zoneMode, String zoneName) { + HashMap extFields = new HashMap<>(); + extFields.put(MixAll.ZONE_MODE, zoneMode); + extFields.put(MixAll.ZONE_NAME, zoneName); + return extFields; + } + +} \ No newline at end of file diff --git a/namesrv/src/test/java/org/apache/rocketmq/namesrv/route/ZoneRouteRPCHookTest.java b/namesrv/src/test/java/org/apache/rocketmq/namesrv/route/ZoneRouteRPCHookTest.java new file mode 100644 index 0000000..1bf4a6c --- /dev/null +++ b/namesrv/src/test/java/org/apache/rocketmq/namesrv/route/ZoneRouteRPCHookTest.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.namesrv.route; + +import org.apache.rocketmq.common.MixAll; +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.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.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + + +public class ZoneRouteRPCHookTest { + + private ZoneRouteRPCHook zoneRouteRPCHook; + + @Before + public void setup() { + zoneRouteRPCHook = new ZoneRouteRPCHook(); + } + + @Test + public void testDoAfterResponseWithNoZoneMode() { + RemotingCommand request1 = RemotingCommand.createRequestCommand(106,null); + zoneRouteRPCHook.doAfterResponse("", request1, null); + + HashMap extFields = new HashMap<>(); + extFields.put(MixAll.ZONE_MODE, "false"); + RemotingCommand request = RemotingCommand.createRequestCommand(105,null); + request.setExtFields(extFields); + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + response.setBody(RemotingSerializable.encode(createSampleTopicRouteData())); + zoneRouteRPCHook.doAfterResponse("", request, response); + } + + @Test + public void testDoAfterResponseWithNoZoneName() { + HashMap extFields = new HashMap<>(); + extFields.put(MixAll.ZONE_MODE, "true"); + RemotingCommand request = RemotingCommand.createRequestCommand(105,null); + request.setExtFields(extFields); + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + response.setBody(RemotingSerializable.encode(createSampleTopicRouteData())); + zoneRouteRPCHook.doAfterResponse("", request, response); + } + + @Test + public void testDoAfterResponseWithNoResponse() { + HashMap extFields = new HashMap<>(); + extFields.put(MixAll.ZONE_MODE, "true"); + RemotingCommand request = RemotingCommand.createRequestCommand(105,null); + request.setExtFields(extFields); + zoneRouteRPCHook.doAfterResponse("", request, null); + + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + zoneRouteRPCHook.doAfterResponse("", request, response); + + response.setBody(RemotingSerializable.encode(createSampleTopicRouteData())); + response.setCode(ResponseCode.NO_PERMISSION); + zoneRouteRPCHook.doAfterResponse("", request, response); + } + + + @Test + public void testDoAfterResponseWithValidZoneFiltering() throws Exception { + HashMap extFields = new HashMap<>(); + extFields.put(MixAll.ZONE_MODE, "true"); + extFields.put(MixAll.ZONE_NAME,"zone1"); + RemotingCommand request = RemotingCommand.createRequestCommand(105,null); + request.setExtFields(extFields); + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + TopicRouteData topicRouteData = createSampleTopicRouteData(); + response.setBody(RemotingSerializable.encode(topicRouteData)); + zoneRouteRPCHook.doAfterResponse("", request, response); + + HashMap brokeraddrs = new HashMap<>(); + brokeraddrs.put(MixAll.MASTER_ID,"127.0.0.1:10911"); + topicRouteData.getBrokerDatas().get(0).setBrokerAddrs(brokeraddrs); + response.setBody(RemotingSerializable.encode(topicRouteData)); + zoneRouteRPCHook.doAfterResponse("", request, response); + + topicRouteData.getQueueDatas().add(createQueueData("BrokerB")); + HashMap brokeraddrsB = new HashMap<>(); + brokeraddrsB.put(MixAll.MASTER_ID,"127.0.0.1:10912"); + BrokerData brokerData1 = createBrokerData("BrokerB","zone2",brokeraddrsB); + BrokerData brokerData2 = createBrokerData("BrokerC","zone1",null); + topicRouteData.getBrokerDatas().add(brokerData1); + topicRouteData.getBrokerDatas().add(brokerData2); + response.setBody(RemotingSerializable.encode(topicRouteData)); + zoneRouteRPCHook.doAfterResponse("", request, response); + + topicRouteData.getFilterServerTable().put("127.0.0.1:10911",new ArrayList<>()); + response.setBody(RemotingSerializable.encode(topicRouteData)); + zoneRouteRPCHook.doAfterResponse("", request, response); + Assert.assertEquals(1,RemotingSerializable + .decode(response.getBody(), TopicRouteData.class) + .getFilterServerTable() + .size()); + + topicRouteData.getFilterServerTable().put("127.0.0.1:10912",new ArrayList<>()); + response.setBody(RemotingSerializable.encode(topicRouteData)); + zoneRouteRPCHook.doAfterResponse("", request, response); + Assert.assertEquals(1,RemotingSerializable + .decode(response.getBody(), TopicRouteData.class) + .getFilterServerTable() + .size()); + } + + private TopicRouteData createSampleTopicRouteData() { + TopicRouteData topicRouteData = new TopicRouteData(); + List brokerDatas = new ArrayList<>(); + BrokerData brokerData = createBrokerData("BrokerA","zone1",new HashMap<>()); + List queueDatas = new ArrayList<>(); + QueueData queueData = createQueueData("BrokerA"); + queueDatas.add(queueData); + brokerDatas.add(brokerData); + topicRouteData.setBrokerDatas(brokerDatas); + topicRouteData.setQueueDatas(queueDatas); + return topicRouteData; + } + + private BrokerData createBrokerData(String brokerName,String zoneName,HashMap brokerAddrs) { + BrokerData brokerData = new BrokerData(); + brokerData.setBrokerName(brokerName); + brokerData.setZoneName(zoneName); + brokerData.setBrokerAddrs(brokerAddrs); + return brokerData; + } + + private QueueData createQueueData(String brokerName) { + QueueData queueData = new QueueData(); + queueData.setBrokerName(brokerName); + queueData.setReadQueueNums(8); + queueData.setWriteQueueNums(8); + queueData.setPerm(6); + return queueData; + } +} diff --git a/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/BrokerHousekeepingServiceTest.java b/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/BrokerHousekeepingServiceTest.java new file mode 100644 index 0000000..ce6ce23 --- /dev/null +++ b/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/BrokerHousekeepingServiceTest.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.namesrv.routeinfo; + +import org.apache.rocketmq.common.namesrv.NamesrvConfig; +import org.apache.rocketmq.namesrv.NamesrvController; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +public class BrokerHousekeepingServiceTest { + private static BrokerHousekeepingService brokerHousekeepingService; + + @BeforeClass + public static void setup() { + NamesrvController namesrvController = new NamesrvController( + new NamesrvConfig(), + new NettyServerConfig() + ); + brokerHousekeepingService = new BrokerHousekeepingService(namesrvController); + } + + @AfterClass + public static void terminate() { + + } + + @Test + public void testOnChannelClose() { + brokerHousekeepingService.onChannelClose("127.0.0.1:9876", null); + } + + @Test + public void testOnChannelException() { + brokerHousekeepingService.onChannelException("127.0.0.1:9876", null); + } + + @Test + public void testOnChannelIdle() { + brokerHousekeepingService.onChannelException("127.0.0.1:9876", null); + } + +} \ No newline at end of file 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 0000000..b453f4d --- /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 0000000..0e9cf67 --- /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 0000000..5a2fab8 --- /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 0000000..aa616e6 --- /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 0000000..5e58cfc --- /dev/null +++ b/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManagerNewTest.java @@ -0,0 +1,949 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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() throws InterruptedException { + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), "TestTopic"); + routeInfoManager.scanNotActiveBroker(); + registerBrokerWithNormalTopicAndExpire(BrokerBasicInfo.defaultBroker(),"TestTopic"); + Thread.sleep(30000); + 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 onChannelDestroyByBrokerInfo() { + registerBroker(BrokerBasicInfo.defaultBroker(), mock(Channel.class), null, "TestTopic", "TestTopic1"); + BrokerAddrInfo brokerAddrInfo = new BrokerAddrInfo(DEFAULT_CLUSTER, DEFAULT_ADDR); + routeInfoManager.onChannelDestroy(brokerAddrInfo); + await().atMost(Duration.ofSeconds(5)).until(() -> routeInfoManager.blockedUnRegisterRequests() == 0); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic")).isNull(); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic1")).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 registerBrokerWithNormalTopicAndExpire(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 registerBrokerWithExpiredTime(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); + + return routeInfoManager.registerBroker( + brokerInfo.clusterName, + brokerInfo.brokerAddr, + brokerInfo.brokerName, + brokerInfo.brokerId, + brokerInfo.haAddr, + "", + null, + brokerInfo.enableActingMaster, + topicConfigSerializeWrapper, new ArrayList<>(), channel); + } + + private RegisterBrokerResult registerBrokerWithExpiredTime(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); + + return routeInfoManager.registerBroker( + brokerInfo.clusterName, + brokerInfo.brokerAddr, + brokerInfo.brokerName, + brokerInfo.brokerId, + brokerInfo.haAddr, + "", + 30000L, + brokerInfo.enableActingMaster, + topicConfigSerializeWrapper, new ArrayList<>(), channel); + } + + 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 0000000..6c90e7b --- /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 new file mode 100644 index 0000000..d9ac9e4 --- /dev/null +++ b/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManagerTest.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.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.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; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +public class RouteInfoManagerTest { + + private static RouteInfoManager routeInfoManager; + + @Before + public void setup() { + 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().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().encode(); + Assert.assertTrue(topicInfo != null); + assertThat(topicInfo).isNotNull(); + } + + @Test + public void testRegisterBroker() { + DataVersion dataVersion = new DataVersion(); + dataVersion.setCounter(new AtomicLong(10L)); + dataVersion.setTimestamp(100L); + + ConcurrentHashMap topicConfigConcurrentHashMap = new ConcurrentHashMap<>(); + 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", "", + null, topicConfigSerializeWrapper, new ArrayList<>(), channel); + assertThat(registerBrokerResult).isNotNull(); + } + + @Test + 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 + public void testPickupTopicRouteData() { + TopicRouteData result = routeInfoManager.pickupTopicRouteData("unit_test"); + assertThat(result).isNull(); + } + + @Test + public void testGetSystemTopicList() { + byte[] topicList = routeInfoManager.getSystemTopicList().encode(); + assertThat(topicList).isNotNull(); + } + + @Test + public void testGetTopicsByCluster() { + byte[] topicList = routeInfoManager.getTopicsByCluster("default-cluster").encode(); + assertThat(topicList).isNotNull(); + } + + @Test + public void testGetUnitTopics() { + byte[] topicList = routeInfoManager.getUnitTopics().encode(); + assertThat(topicList).isNotNull(); + } + + @Test + public void testGetHasUnitSubTopicList() { + byte[] topicList = routeInfoManager.getHasUnitSubTopicList().encode(); + assertThat(topicList).isNotNull(); + } + + @Test + public void testGetHasUnitSubUnUnitTopicList() { + byte[] topicList = routeInfoManager.getHasUnitSubUnUnitTopicList().encode(); + assertThat(topicList).isNotNull(); + } + + @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 0000000..a5faeea --- /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 0000000..8695d52 --- /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 new file mode 100644 index 0000000..f07cea8 --- /dev/null +++ b/openmessaging/pom.xml @@ -0,0 +1,45 @@ + + + + + + rocketmq-all + org.apache.rocketmq + 5.3.4-SNAPSHOT + + + 4.0.0 + rocketmq-openmessaging + rocketmq-openmessaging ${project.version} + + + ${basedir}/.. + + + + + io.openmessaging + openmessaging-api + + + ${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 new file mode 100644 index 0000000..51388f9 --- /dev/null +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/MessagingAccessPointImpl.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 io.openmessaging.rocketmq; + +import io.openmessaging.KeyValue; +import io.openmessaging.MessagingAccessPoint; +import io.openmessaging.ResourceManager; +import io.openmessaging.consumer.PullConsumer; +import io.openmessaging.consumer.PushConsumer; +import io.openmessaging.consumer.StreamingConsumer; +import io.openmessaging.exception.OMSNotSupportedException; +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.utils.OMSUtil; + +public class MessagingAccessPointImpl implements MessagingAccessPoint { + + private final KeyValue accessPointProperties; + + public MessagingAccessPointImpl(final KeyValue accessPointProperties) { + this.accessPointProperties = accessPointProperties; + } + + @Override + public KeyValue attributes() { + return accessPointProperties; + } + + @Override + public String implVersion() { + return "0.3.0"; + } + + @Override + public Producer createProducer() { + return new ProducerImpl(this.accessPointProperties); + } + + @Override + public Producer createProducer(KeyValue properties) { + return new ProducerImpl(OMSUtil.buildKeyValue(this.accessPointProperties, properties)); + } + + @Override + public PushConsumer createPushConsumer() { + return new PushConsumerImpl(accessPointProperties); + } + + @Override + public PushConsumer createPushConsumer(KeyValue properties) { + return new PushConsumerImpl(OMSUtil.buildKeyValue(this.accessPointProperties, properties)); + } + + @Override + public PullConsumer createPullConsumer() { + return new PullConsumerImpl(accessPointProperties); + } + + @Override + public PullConsumer createPullConsumer(KeyValue attributes) { + return new PullConsumerImpl(OMSUtil.buildKeyValue(this.accessPointProperties, attributes)); + } + + @Override + public StreamingConsumer createStreamingConsumer() { + return null; + } + + @Override + public StreamingConsumer createStreamingConsumer(KeyValue attributes) { + return null; + } + + @Override + public ResourceManager resourceManager() { + throw new OMSNotSupportedException("-1", "ResourceManager is not supported in current version."); + } + + @Override + public void startup() { + //Ignore + } + + @Override + public void shutdown() { + //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 new file mode 100644 index 0000000..a5dfe49 --- /dev/null +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/config/ClientConfig.java @@ -0,0 +1,194 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.config; + +import io.openmessaging.OMSBuiltinKeys; +import io.openmessaging.rocketmq.domain.NonStandardKeys; + +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; + private int rmqMessageConsumeTimeout = 15; //In minutes + private int rmqMaxConsumeThreadNums = 64; + private int rmqMinConsumeThreadNums = 20; + private String rmqMessageDestination; + private int rmqPullMessageBatchNums = 32; + private int rmqPullMessageCacheCapacity = 1000; + + public String getDriverImpl() { + return driverImpl; + } + + public void setDriverImpl(final String driverImpl) { + this.driverImpl = driverImpl; + } + + public String getAccessPoints() { + return accessPoints; + } + + public void setAccessPoints(final String accessPoints) { + this.accessPoints = accessPoints; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(final String namespace) { + this.namespace = namespace; + } + + public String getProducerId() { + return producerId; + } + + public void setProducerId(final String producerId) { + this.producerId = producerId; + } + + public String getConsumerId() { + return consumerId; + } + + public void setConsumerId(final String consumerId) { + this.consumerId = consumerId; + } + + public int getOperationTimeout() { + return operationTimeout; + } + + public void setOperationTimeout(final int operationTimeout) { + this.operationTimeout = operationTimeout; + } + + public String getRoutingSource() { + return routingSource; + } + + public void setRoutingSource(final String routingSource) { + this.routingSource = routingSource; + } + + public String getRmqConsumerGroup() { + return rmqConsumerGroup; + } + + public void setRmqConsumerGroup(final String rmqConsumerGroup) { + this.rmqConsumerGroup = rmqConsumerGroup; + } + + public String getRmqProducerGroup() { + return rmqProducerGroup; + } + + public void setRmqProducerGroup(final String rmqProducerGroup) { + this.rmqProducerGroup = rmqProducerGroup; + } + + public int getRmqMaxRedeliveryTimes() { + return rmqMaxRedeliveryTimes; + } + + public void setRmqMaxRedeliveryTimes(final int rmqMaxRedeliveryTimes) { + this.rmqMaxRedeliveryTimes = rmqMaxRedeliveryTimes; + } + + public int getRmqMessageConsumeTimeout() { + return rmqMessageConsumeTimeout; + } + + public void setRmqMessageConsumeTimeout(final int rmqMessageConsumeTimeout) { + this.rmqMessageConsumeTimeout = rmqMessageConsumeTimeout; + } + + public int getRmqMaxConsumeThreadNums() { + return rmqMaxConsumeThreadNums; + } + + public void setRmqMaxConsumeThreadNums(final int rmqMaxConsumeThreadNums) { + this.rmqMaxConsumeThreadNums = rmqMaxConsumeThreadNums; + } + + public int getRmqMinConsumeThreadNums() { + return rmqMinConsumeThreadNums; + } + + public void setRmqMinConsumeThreadNums(final int rmqMinConsumeThreadNums) { + this.rmqMinConsumeThreadNums = rmqMinConsumeThreadNums; + } + + public String getRmqMessageDestination() { + return rmqMessageDestination; + } + + public void setRmqMessageDestination(final String rmqMessageDestination) { + this.rmqMessageDestination = rmqMessageDestination; + } + + public int getRmqPullMessageBatchNums() { + return rmqPullMessageBatchNums; + } + + public void setRmqPullMessageBatchNums(final int rmqPullMessageBatchNums) { + this.rmqPullMessageBatchNums = rmqPullMessageBatchNums; + } + + public int getRmqPullMessageCacheCapacity() { + return rmqPullMessageCacheCapacity; + } + + 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 new file mode 100644 index 0000000..3b2d014 --- /dev/null +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/consumer/LocalMessageCache.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 io.openmessaging.rocketmq.consumer; + +import io.openmessaging.KeyValue; +import io.openmessaging.Message; +import io.openmessaging.ServiceLifecycle; +import io.openmessaging.rocketmq.config.ClientConfig; +import io.openmessaging.rocketmq.domain.ConsumeRequest; +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReadWriteLock; +import org.apache.commons.lang3.reflect.FieldUtils; +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.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.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; + private final DefaultMQPullConsumer rocketmqPullConsumer; + private final ClientConfig clientConfig; + private final ScheduledExecutorService cleanExpireMsgExecutors; + + LocalMessageCache(final DefaultMQPullConsumer rocketmqPullConsumer, final ClientConfig clientConfig) { + consumeRequestCache = new LinkedBlockingQueue<>(clientConfig.getRmqPullMessageCacheCapacity()); + this.consumedRequest = new ConcurrentHashMap<>(); + this.pullOffsetTable = new ConcurrentHashMap<>(); + this.rocketmqPullConsumer = rocketmqPullConsumer; + this.clientConfig = clientConfig; + this.cleanExpireMsgExecutors = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl( + "OMS_CleanExpireMsgScheduledThread_")); + } + + int nextPullBatchNums() { + return Math.min(clientConfig.getRmqPullMessageBatchNums(), consumeRequestCache.remainingCapacity()); + } + + long nextPullOffset(MessageQueue remoteQueue) { + if (!pullOffsetTable.containsKey(remoteQueue)) { + try { + pullOffsetTable.putIfAbsent(remoteQueue, + rocketmqPullConsumer.fetchConsumeOffset(remoteQueue, false)); + } catch (MQClientException e) { + log.error("An error occurred in fetch consume offset process.", e); + } + } + return pullOffsetTable.get(remoteQueue); + } + + void updatePullOffset(MessageQueue remoteQueue, long nextPullOffset) { + pullOffsetTable.put(remoteQueue, nextPullOffset); + } + + void submitConsumeRequest(ConsumeRequest consumeRequest) { + try { + consumeRequestCache.put(consumeRequest); + } catch (InterruptedException ignore) { + } + } + + MessageExt poll() { + return poll(clientConfig.getOperationTimeout()); + } + + MessageExt poll(final KeyValue properties) { + int currentPollTimeout = clientConfig.getOperationTimeout(); + if (properties.containsKey(Message.BuiltinKeys.TIMEOUT)) { + currentPollTimeout = properties.getInt(Message.BuiltinKeys.TIMEOUT); + } + return poll(currentPollTimeout); + } + + private MessageExt poll(long timeout) { + try { + ConsumeRequest consumeRequest = consumeRequestCache.poll(timeout, TimeUnit.MILLISECONDS); + if (consumeRequest != null) { + MessageExt messageExt = consumeRequest.getMessageExt(); + consumeRequest.setStartConsumeTimeMillis(System.currentTimeMillis()); + MessageAccessor.setConsumeStartTimeStamp(messageExt, String.valueOf(consumeRequest.getStartConsumeTimeMillis())); + consumedRequest.put(messageExt.getMsgId(), consumeRequest); + return messageExt; + } + } catch (InterruptedException ignore) { + } + return null; + } + + void ack(final String messageId) { + ConsumeRequest consumeRequest = consumedRequest.remove(messageId); + if (consumeRequest != null) { + long offset = consumeRequest.getProcessQueue().removeMessage(Collections.singletonList(consumeRequest.getMessageExt())); + try { + rocketmqPullConsumer.updateConsumeOffset(consumeRequest.getMessageQueue(), offset); + } catch (MQClientException e) { + log.error("An error occurred in update consume offset process.", e); + } + } + } + + void ack(final MessageQueue messageQueue, final ProcessQueue processQueue, final MessageExt messageExt) { + consumedRequest.remove(messageExt.getMsgId()); + long offset = processQueue.removeMessage(Collections.singletonList(messageExt)); + try { + rocketmqPullConsumer.updateConsumeOffset(messageQueue, offset); + } catch (MQClientException e) { + log.error("An error occurred in update consume offset process.", e); + } + } + + @Override + public void startup() { + this.cleanExpireMsgExecutors.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + cleanExpireMsg(); + } + }, clientConfig.getRmqMessageConsumeTimeout(), clientConfig.getRmqMessageConsumeTimeout(), TimeUnit.MINUTES); + } + + @Override + public void shutdown() { + ThreadUtils.shutdownGracefully(cleanExpireMsgExecutors, 5000, TimeUnit.MILLISECONDS); + } + + private void cleanExpireMsg() { + for (final Map.Entry next : rocketmqPullConsumer.getDefaultMQPullConsumerImpl() + .getRebalanceImpl().getProcessQueueTable().entrySet()) { + ProcessQueue pq = next.getValue(); + MessageQueue mq = next.getKey(); + ReadWriteLock lockTreeMap = getLockInProcessQueue(pq); + if (lockTreeMap == null) { + log.error("Gets tree map lock in process queue error, may be has compatibility issue"); + return; + } + + TreeMap msgTreeMap = pq.getMsgTreeMap(); + + int loop = msgTreeMap.size(); + for (int i = 0; i < loop; i++) { + MessageExt msg = null; + try { + lockTreeMap.readLock().lockInterruptibly(); + try { + if (!msgTreeMap.isEmpty()) { + msg = msgTreeMap.firstEntry().getValue(); + if (System.currentTimeMillis() - Long.parseLong(MessageAccessor.getConsumeStartTimeStamp(msg)) + > clientConfig.getRmqMessageConsumeTimeout() * 60 * 1000) { + //Expired, ack and remove it. + } else { + break; + } + } else { + break; + } + } finally { + lockTreeMap.readLock().unlock(); + } + } catch (InterruptedException e) { + log.error("Gets expired message exception", e); + } + + try { + rocketmqPullConsumer.sendMessageBack(msg, 3); + log.info("Send expired msg back. topic={}, msgId={}, storeHost={}, queueId={}, queueOffset={}", + msg.getTopic(), msg.getMsgId(), msg.getStoreHost(), msg.getQueueId(), msg.getQueueOffset()); + ack(mq, pq, msg); + } catch (Exception e) { + log.error("Send back expired msg exception", e); + } + } + } + } + + private ReadWriteLock getLockInProcessQueue(ProcessQueue pq) { + try { + return (ReadWriteLock) FieldUtils.readDeclaredField(pq, "lockTreeMap", true); + } catch (IllegalAccessException e) { + return null; + } + } +} diff --git a/openmessaging/src/main/java/io/openmessaging/rocketmq/consumer/PullConsumerImpl.java b/openmessaging/src/main/java/io/openmessaging/rocketmq/consumer/PullConsumerImpl.java new file mode 100644 index 0000000..670d1ab --- /dev/null +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/consumer/PullConsumerImpl.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 io.openmessaging.rocketmq.consumer; + +import io.openmessaging.KeyValue; +import io.openmessaging.Message; +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; +import io.openmessaging.rocketmq.utils.BeanUtils; +import io.openmessaging.rocketmq.utils.OMSUtil; +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.client.consumer.MQPullConsumer; +import org.apache.rocketmq.client.consumer.MQPullConsumerScheduleService; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.consumer.PullTaskCallback; +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.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +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 final MQPullConsumerScheduleService pullConsumerScheduleService; + private final LocalMessageCache localMessageCache; + private final ClientConfig clientConfig; + + public PullConsumerImpl(final KeyValue properties) { + this.properties = properties; + this.clientConfig = BeanUtils.populate(properties, ClientConfig.class); + + String consumerGroup = clientConfig.getConsumerId(); + if (null == consumerGroup || consumerGroup.isEmpty()) { + throw new OMSRuntimeException("-1", "Consumer Group is necessary for RocketMQ, please set it."); + } + pullConsumerScheduleService = new MQPullConsumerScheduleService(consumerGroup); + + this.rocketmqPullConsumer = pullConsumerScheduleService.getDefaultMQPullConsumer(); + + 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.setConsumerGroup(consumerGroup); + + int maxReDeliveryTimes = clientConfig.getRmqMaxRedeliveryTimes(); + this.rocketmqPullConsumer.setMaxReconsumeTimes(maxReDeliveryTimes); + + String consumerId = OMSUtil.buildInstanceName(); + this.rocketmqPullConsumer.setInstanceName(consumerId); + properties.put(OMSBuiltinKeys.CONSUMER_ID, consumerId); + + this.rocketmqPullConsumer.setLanguage(LanguageCode.OMS); + + this.localMessageCache = new LocalMessageCache(this.rocketmqPullConsumer, clientConfig); + } + + @Override + public KeyValue attributes() { + return properties; + } + + @Override + 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 receive(final KeyValue properties) { + MessageExt rmqMsg = localMessageCache.poll(properties); + return rmqMsg == null ? null : OMSUtil.msgConvert(rmqMsg); + } + + @Override + public void ack(final String messageId) { + localMessageCache.ack(messageId); + } + + @Override + public void ack(final String messageId, final KeyValue properties) { + localMessageCache.ack(messageId); + } + + @Override + public synchronized void startup() { + if (!started) { + try { + this.pullConsumerScheduleService.start(); + this.localMessageCache.startup(); + } catch (MQClientException e) { + throw new OMSRuntimeException("-1", e); + } + } + this.started = true; + } + + private void registerPullTaskCallback(final String targetQueueName) { + this.pullConsumerScheduleService.registerPullTaskCallback(targetQueueName, new PullTaskCallback() { + @Override + public void doPullTask(final MessageQueue mq, final PullTaskContext context) { + MQPullConsumer consumer = context.getPullConsumer(); + try { + long offset = localMessageCache.nextPullOffset(mq); + + PullResult pullResult = consumer.pull(mq, "*", + offset, localMessageCache.nextPullBatchNums()); + ProcessQueue pq = rocketmqPullConsumer.getDefaultMQPullConsumerImpl().getRebalanceImpl() + .getProcessQueueTable().get(mq); + switch (pullResult.getPullStatus()) { + case FOUND: + if (pq != null) { + pq.putMessage(pullResult.getMsgFoundList()); + for (final MessageExt messageExt : pullResult.getMsgFoundList()) { + localMessageCache.submitConsumeRequest(new ConsumeRequest(messageExt, mq, pq)); + } + } + break; + default: + break; + } + localMessageCache.updatePullOffset(mq, pullResult.getNextBeginOffset()); + } catch (Exception e) { + log.error("An error occurred in pull message process.", e); + } + } + }); + } + + @Override + public synchronized void shutdown() { + if (this.started) { + this.localMessageCache.shutdown(); + this.pullConsumerScheduleService.shutdown(); + this.rocketmqPullConsumer.shutdown(); + } + this.started = false; + } +} diff --git a/openmessaging/src/main/java/io/openmessaging/rocketmq/consumer/PushConsumerImpl.java b/openmessaging/src/main/java/io/openmessaging/rocketmq/consumer/PushConsumerImpl.java new file mode 100644 index 0000000..1675a16 --- /dev/null +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/consumer/PushConsumerImpl.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 io.openmessaging.rocketmq.consumer; + +import io.openmessaging.BytesMessage; +import io.openmessaging.KeyValue; +import io.openmessaging.OMS; +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; +import io.openmessaging.rocketmq.utils.OMSUtil; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +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; +import org.apache.rocketmq.remoting.protocol.LanguageCode; + +public class PushConsumerImpl implements PushConsumer { + private final DefaultMQPushConsumer rocketmqPushConsumer; + private final KeyValue properties; + private boolean started = false; + private final Map subscribeTable = new ConcurrentHashMap<>(); + private final ClientConfig clientConfig; + + public PushConsumerImpl(final KeyValue properties) { + this.rocketmqPushConsumer = new DefaultMQPushConsumer(); + this.properties = properties; + this.clientConfig = BeanUtils.populate(properties, ClientConfig.class); + + 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(',', ';')); + } + + String consumerGroup = clientConfig.getConsumerId(); + if (null == consumerGroup || consumerGroup.isEmpty()) { + throw new OMSRuntimeException("-1", "Consumer Group is necessary for RocketMQ, please set it."); + } + this.rocketmqPushConsumer.setConsumerGroup(consumerGroup); + this.rocketmqPushConsumer.setMaxReconsumeTimes(clientConfig.getRmqMaxRedeliveryTimes()); + this.rocketmqPushConsumer.setConsumeTimeout(clientConfig.getRmqMessageConsumeTimeout()); + this.rocketmqPushConsumer.setConsumeThreadMax(clientConfig.getRmqMaxConsumeThreadNums()); + this.rocketmqPushConsumer.setConsumeThreadMin(clientConfig.getRmqMinConsumeThreadNums()); + + String consumerId = OMSUtil.buildInstanceName(); + this.rocketmqPushConsumer.setInstanceName(consumerId); + properties.put(OMSBuiltinKeys.CONSUMER_ID, consumerId); + this.rocketmqPushConsumer.setLanguage(LanguageCode.OMS); + + this.rocketmqPushConsumer.registerMessageListener(new MessageListenerImpl()); + } + + @Override + public KeyValue attributes() { + return properties; + } + + @Override + public void resume() { + this.rocketmqPushConsumer.resume(); + } + + @Override + public void suspend() { + this.rocketmqPushConsumer.suspend(); + } + + @Override + public void suspend(long timeout) { + + } + + @Override + public boolean isSuspended() { + return this.rocketmqPushConsumer.isPause(); + } + + @Override + public PushConsumer attachQueue(final String queueName, final MessageListener listener) { + this.subscribeTable.put(queueName, listener); + try { + this.rocketmqPushConsumer.subscribe(queueName, "*"); + } catch (MQClientException e) { + throw new OMSRuntimeException("-1", String.format("RocketMQ push consumer can't attach to %s.", queueName)); + } + 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) { + try { + this.rocketmqPushConsumer.start(); + } catch (MQClientException e) { + throw new OMSRuntimeException("-1", e); + } + } + this.started = true; + } + + @Override + public synchronized void shutdown() { + if (this.started) { + this.rocketmqPushConsumer.shutdown(); + } + this.started = false; + } + + class MessageListenerImpl implements MessageListenerConcurrently { + + @Override + public ConsumeConcurrentlyStatus consumeMessage(List rmqMsgList, + ConsumeConcurrentlyContext contextRMQ) { + MessageExt rmqMsg = rmqMsgList.get(0); + BytesMessage omsMsg = OMSUtil.msgConvert(rmqMsg); + + MessageListener listener = PushConsumerImpl.this.subscribeTable.get(rmqMsg.getTopic()); + + if (listener == null) { + throw new OMSRuntimeException("-1", + String.format("The topic/queue %s isn't attached to this consumer", rmqMsg.getTopic())); + } + + final KeyValue contextProperties = OMS.newKeyValue(); + final CountDownLatch sync = new CountDownLatch(1); + + contextProperties.put(NonStandardKeys.MESSAGE_CONSUME_STATUS, ConsumeConcurrentlyStatus.RECONSUME_LATER.name()); + + MessageListener.Context context = new MessageListener.Context() { + @Override + public KeyValue attributes() { + return contextProperties; + } + + @Override + public void ack() { + sync.countDown(); + contextProperties.put(NonStandardKeys.MESSAGE_CONSUME_STATUS, + ConsumeConcurrentlyStatus.CONSUME_SUCCESS.name()); + } + }; + long begin = System.currentTimeMillis(); + listener.onReceived(omsMsg, context); + long costs = System.currentTimeMillis() - begin; + long timeoutMills = clientConfig.getRmqMessageConsumeTimeout() * 60 * 1000; + try { + sync.await(Math.max(0, timeoutMills - costs), TimeUnit.MILLISECONDS); + } catch (InterruptedException ignore) { + } + + return ConsumeConcurrentlyStatus.valueOf(contextProperties.getString(NonStandardKeys.MESSAGE_CONSUME_STATUS)); + } + } +} diff --git a/openmessaging/src/main/java/io/openmessaging/rocketmq/domain/BytesMessageImpl.java b/openmessaging/src/main/java/io/openmessaging/rocketmq/domain/BytesMessageImpl.java new file mode 100644 index 0000000..6d8995a --- /dev/null +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/domain/BytesMessageImpl.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 io.openmessaging.rocketmq.domain; + +import io.openmessaging.BytesMessage; +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 sysHeaders; + private KeyValue userHeaders; + private byte[] body; + + public BytesMessageImpl() { + this.sysHeaders = OMS.newKeyValue(); + this.userHeaders = OMS.newKeyValue(); + } + + @Override + public T getBody(Class type) throws OMSMessageFormatException { + if (type == byte[].class) { + return (T)body; + } + + throw new OMSMessageFormatException("", "Cannot assign byte[] to " + type.getName()); + } + + @Override + public BytesMessage setBody(final byte[] body) { + this.body = body; + return this; + } + + @Override + public KeyValue sysHeaders() { + return sysHeaders; + } + + @Override + public KeyValue userHeaders() { + return userHeaders; + } + + @Override + public Message putSysHeaders(String key, int value) { + sysHeaders.put(key, value); + return this; + } + + @Override + public Message putSysHeaders(String key, long value) { + sysHeaders.put(key, value); + return this; + } + + @Override + public Message putSysHeaders(String key, double value) { + sysHeaders.put(key, value); + return this; + } + + @Override + public Message putSysHeaders(String key, String value) { + sysHeaders.put(key, value); + return this; + } + + @Override + public Message putUserHeaders(String key, int value) { + userHeaders.put(key, value); + return this; + } + + @Override + public Message putUserHeaders(String key, long value) { + userHeaders.put(key, value); + return this; + } + + @Override + public Message putUserHeaders(String key, double value) { + userHeaders.put(key, value); + return this; + } + + @Override + public Message putUserHeaders(String key, String value) { + userHeaders.put(key, value); + return this; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this); + } +} diff --git a/openmessaging/src/main/java/io/openmessaging/rocketmq/domain/ConsumeRequest.java b/openmessaging/src/main/java/io/openmessaging/rocketmq/domain/ConsumeRequest.java new file mode 100644 index 0000000..7ce4a9b --- /dev/null +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/domain/ConsumeRequest.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 io.openmessaging.rocketmq.domain; + +import org.apache.rocketmq.client.impl.consumer.ProcessQueue; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; + +public class ConsumeRequest { + private final MessageExt messageExt; + private final MessageQueue messageQueue; + private final ProcessQueue processQueue; + private long startConsumeTimeMillis; + + public ConsumeRequest(final MessageExt messageExt, final MessageQueue messageQueue, + final ProcessQueue processQueue) { + this.messageExt = messageExt; + this.messageQueue = messageQueue; + this.processQueue = processQueue; + } + + public MessageExt getMessageExt() { + return messageExt; + } + + public MessageQueue getMessageQueue() { + return messageQueue; + } + + public ProcessQueue getProcessQueue() { + return processQueue; + } + + public long getStartConsumeTimeMillis() { + return startConsumeTimeMillis; + } + + public void setStartConsumeTimeMillis(final long startConsumeTimeMillis) { + this.startConsumeTimeMillis = startConsumeTimeMillis; + } +} diff --git a/openmessaging/src/main/java/io/openmessaging/rocketmq/domain/NonStandardKeys.java b/openmessaging/src/main/java/io/openmessaging/rocketmq/domain/NonStandardKeys.java new file mode 100644 index 0000000..3639a3f --- /dev/null +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/domain/NonStandardKeys.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 io.openmessaging.rocketmq.domain; + +public interface NonStandardKeys { + String CONSUMER_GROUP = "rmq.consumer.group"; + String PRODUCER_GROUP = "rmq.producer.group"; + String MAX_REDELIVERY_TIMES = "rmq.max.redelivery.times"; + String MESSAGE_CONSUME_TIMEOUT = "rmq.message.consume.timeout"; + String MAX_CONSUME_THREAD_NUMS = "rmq.max.consume.thread.nums"; + String MIN_CONSUME_THREAD_NUMS = "rmq.min.consume.thread.nums"; + String MESSAGE_CONSUME_STATUS = "rmq.message.consume.status"; + String MESSAGE_DESTINATION = "rmq.message.destination"; + String PULL_MESSAGE_BATCH_NUMS = "rmq.pull.message.batch.nums"; + String PULL_MESSAGE_CACHE_CAPACITY = "rmq.pull.message.cache.capacity"; +} 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 0000000..2bebc8a --- /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 new file mode 100644 index 0000000..85bcd68 --- /dev/null +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/domain/SendResultImpl.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 io.openmessaging.rocketmq.domain; + +import io.openmessaging.KeyValue; +import io.openmessaging.producer.SendResult; + +public class SendResultImpl implements SendResult { + private String messageId; + private KeyValue properties; + + public SendResultImpl(final String messageId, final KeyValue properties) { + this.messageId = messageId; + this.properties = properties; + } + + @Override + public String messageId() { + return messageId; + } + + 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 new file mode 100644 index 0000000..e032461 --- /dev/null +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/producer/AbstractOMSProducer.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 io.openmessaging.rocketmq.producer; + +import io.openmessaging.BytesMessage; +import io.openmessaging.KeyValue; +import io.openmessaging.Message; +import io.openmessaging.MessageFactory; +import io.openmessaging.OMSBuiltinKeys; +import io.openmessaging.ServiceLifecycle; +import io.openmessaging.exception.OMSMessageFormatException; +import io.openmessaging.exception.OMSNotSupportedException; +import io.openmessaging.exception.OMSRuntimeException; +import io.openmessaging.exception.OMSTimeOutException; +import io.openmessaging.rocketmq.config.ClientConfig; +import io.openmessaging.rocketmq.domain.BytesMessageImpl; +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.producer.DefaultMQProducer; +import org.apache.rocketmq.remoting.exception.RemotingConnectException; +import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; +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 KeyValue properties; + final DefaultMQProducer rocketmqProducer; + private boolean started = false; + private final ClientConfig clientConfig; + + AbstractOMSProducer(final KeyValue properties) { + this.properties = properties; + this.rocketmqProducer = new DefaultMQProducer(); + this.clientConfig = BeanUtils.populate(properties, ClientConfig.class); + + 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.setProducerGroup(clientConfig.getRmqProducerGroup()); + + String producerId = buildInstanceName(); + this.rocketmqProducer.setSendMsgTimeout(clientConfig.getOperationTimeout()); + this.rocketmqProducer.setInstanceName(producerId); + this.rocketmqProducer.setMaxMessageSize(1024 * 1024 * 4); + this.rocketmqProducer.setLanguage(LanguageCode.OMS); + properties.put(OMSBuiltinKeys.PRODUCER_ID, producerId); + } + + @Override + public synchronized void startup() { + if (!started) { + try { + this.rocketmqProducer.start(); + } catch (MQClientException e) { + throw new OMSRuntimeException("-1", e); + } + } + this.started = true; + } + + @Override + public synchronized void shutdown() { + if (this.started) { + this.rocketmqProducer.shutdown(); + } + this.started = false; + } + + OMSRuntimeException checkProducerException(String topic, String msgId, Throwable e) { + if (e instanceof MQClientException) { + if (e.getCause() != null) { + if (e.getCause() instanceof RemotingTimeoutException) { + 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) { + 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. + else { + MQClientException clientException = (MQClientException) e; + if (-1 == clientException.getResponseCode()) { + return new OMSRuntimeException("-1", String.format("Topic does not exist, Topic=%s, msgId=%s", + topic, msgId), e); + } else if (ResponseCode.MESSAGE_ILLEGAL == clientException.getResponseCode()) { + return new OMSMessageFormatException("-1", String.format("A illegal message for RocketMQ, Topic=%s, msgId=%s", + topic, msgId), e); + } + } + } + return new OMSRuntimeException("-1", "Send message to RocketMQ broker failed.", e); + } + + protected void checkMessageType(Message message) { + if (!(message instanceof BytesMessage)) { + throw new OMSNotSupportedException("-1", "Only BytesMessage is supported."); + } + } + + @Override + 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 new file mode 100644 index 0000000..af712ca --- /dev/null +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/producer/ProducerImpl.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 io.openmessaging.rocketmq.producer; + +import io.openmessaging.BytesMessage; +import io.openmessaging.KeyValue; +import io.openmessaging.Message; +import io.openmessaging.Promise; +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 attributes() { + return properties; + } + + @Override + public SendResult send(final Message message) { + return send(message, this.rocketmqProducer.getSendMsgTimeout()); + } + + @Override + public SendResult send(final Message message, final KeyValue properties) { + 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); + try { + org.apache.rocketmq.client.producer.SendResult rmqResult = this.rocketmqProducer.send(rmqMessage, timeout); + if (!rmqResult.getSendStatus().equals(SendStatus.SEND_OK)) { + log.error(String.format("Send message to RocketMQ failed, %s", message)); + throw new OMSRuntimeException("-1", "Send message to RocketMQ broker failed."); + } + 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.sysHeaders().getString(Message.BuiltinKeys.MESSAGE_ID), e); + } + } + + @Override + public Promise sendAsync(final Message message) { + return sendAsync(message, this.rocketmqProducer.getSendMsgTimeout()); + } + + @Override + public Promise sendAsync(final Message message, final KeyValue properties) { + long timeout = properties.containsKey(Message.BuiltinKeys.TIMEOUT) + ? properties.getInt(Message.BuiltinKeys.TIMEOUT) : this.rocketmqProducer.getSendMsgTimeout(); + return sendAsync(message, timeout); + } + + private Promise sendAsync(final Message message, long timeout) { + checkMessageType(message); + org.apache.rocketmq.common.message.Message rmqMessage = msgConvert((BytesMessage) message); + final Promise promise = new DefaultPromise<>(); + try { + this.rocketmqProducer.send(rmqMessage, new SendCallback() { + @Override + public void onSuccess(final org.apache.rocketmq.client.producer.SendResult rmqResult) { + message.sysHeaders().put(Message.BuiltinKeys.MESSAGE_ID, rmqResult.getMsgId()); + promise.set(OMSUtil.sendResultConvert(rmqResult)); + } + + @Override + public void onException(final Throwable e) { + promise.setFailure(e); + } + }, timeout); + } catch (Exception e) { + promise.setFailure(e); + } + return promise; + } + + @Override + public void sendOneway(final Message message) { + checkMessageType(message); + org.apache.rocketmq.common.message.Message rmqMessage = msgConvert((BytesMessage) message); + try { + this.rocketmqProducer.sendOneway(rmqMessage); + } catch (Exception ignore) { //Ignore the oneway exception. + } + } + + @Override + 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/promise/DefaultPromise.java b/openmessaging/src/main/java/io/openmessaging/rocketmq/promise/DefaultPromise.java new file mode 100644 index 0000000..46e607a --- /dev/null +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/promise/DefaultPromise.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 io.openmessaging.rocketmq.promise; + +import io.openmessaging.Promise; +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; + +public class DefaultPromise implements Promise { + private static final Logger LOG = LoggerFactory.getLogger(DefaultPromise.class); + private final Object lock = new Object(); + private volatile FutureState state = FutureState.DOING; + private V result = null; + private long timeout; + private long createTime; + private Throwable exception = null; + private List> promiseListenerList; + + public DefaultPromise() { + createTime = System.currentTimeMillis(); + promiseListenerList = new ArrayList<>(); + timeout = 5000; + } + + @Override + public boolean cancel(final boolean mayInterruptIfRunning) { + return false; + } + + @Override + public boolean isCancelled() { + return state.isCancelledState(); + } + + @Override + public boolean isDone() { + return state.isDoneState(); + } + + @Override + public V get() { + return result; + } + + @Override + public V get(final long timeout) { + synchronized (lock) { + if (!isDoing()) { + return getValueOrThrowable(); + } + + if (timeout <= 0) { + try { + lock.wait(); + } catch (Exception e) { + cancel(e); + } + return getValueOrThrowable(); + } else { + long waitTime = timeout - (System.currentTimeMillis() - createTime); + if (waitTime > 0) { + for (; ; ) { + try { + lock.wait(waitTime); + } catch (InterruptedException e) { + LOG.error("promise get value interrupted,exception:{}", e.getMessage()); + } + + if (!isDoing()) { + break; + } else { + waitTime = timeout - (System.currentTimeMillis() - createTime); + if (waitTime <= 0) { + break; + } + } + } + } + + if (isDoing()) { + timeoutSoCancel(); + } + } + return getValueOrThrowable(); + } + } + + @Override + public boolean set(final V value) { + if (value == null) + return false; + this.result = value; + return done(); + } + + @Override + public boolean setFailure(final Throwable cause) { + if (cause == null) + return false; + this.exception = cause; + return done(); + } + + @Override + public void addListener(final FutureListener listener) { + if (listener == null) { + throw new NullPointerException("FutureListener is null"); + } + + boolean notifyNow = false; + synchronized (lock) { + if (!isDoing()) { + notifyNow = true; + } else { + if (promiseListenerList == null) { + promiseListenerList = new ArrayList<>(); + } + promiseListenerList.add(listener); + } + } + + if (notifyNow) { + notifyListener(listener); + } + } + + @Override + public Throwable getThrowable() { + return exception; + } + + private void notifyListeners() { + if (promiseListenerList != null) { + for (FutureListener listener : promiseListenerList) { + notifyListener(listener); + } + } + } + + private boolean isSuccess() { + return isDone() && exception == null; + } + + private void timeoutSoCancel() { + synchronized (lock) { + if (!isDoing()) { + return; + } + state = FutureState.CANCELLED; + exception = new RuntimeException("Get request result is timeout or interrupted"); + lock.notifyAll(); + } + notifyListeners(); + } + + private V getValueOrThrowable() { + if (exception != null) { + Throwable e = exception.getCause() != null ? exception.getCause() : exception; + throw new OMSRuntimeException("-1", e); + } + notifyListeners(); + return result; + } + + private boolean isDoing() { + return state.isDoingState(); + } + + private boolean done() { + synchronized (lock) { + if (!isDoing()) { + return false; + } + + state = FutureState.DONE; + lock.notifyAll(); + } + + notifyListeners(); + return true; + } + + private void notifyListener(final FutureListener listener) { + try { + listener.operationComplete(this); + } catch (Throwable t) { + LOG.error("notifyListener {} Error:{}", listener.getClass().getSimpleName(), t); + } + } + + private boolean cancel(Exception e) { + synchronized (lock) { + if (!isDoing()) { + return false; + } + + state = FutureState.CANCELLED; + exception = e; + lock.notifyAll(); + } + + notifyListeners(); + return true; + } +} + diff --git a/openmessaging/src/main/java/io/openmessaging/rocketmq/promise/FutureState.java b/openmessaging/src/main/java/io/openmessaging/rocketmq/promise/FutureState.java new file mode 100644 index 0000000..84b6c2d --- /dev/null +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/promise/FutureState.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 io.openmessaging.rocketmq.promise; + +public enum FutureState { + /** + * the task is doing + **/ + DOING(0), + /** + * the task is done + **/ + DONE(1), + /** + * ths task is cancelled + **/ + CANCELLED(2); + + public final int value; + + private FutureState(int value) { + this.value = value; + } + + public boolean isCancelledState() { + return this == CANCELLED; + } + + public boolean isDoneState() { + return this == DONE; + } + + public boolean isDoingState() { + return this == DOING; + } +} diff --git a/openmessaging/src/main/java/io/openmessaging/rocketmq/utils/BeanUtils.java b/openmessaging/src/main/java/io/openmessaging/rocketmq/utils/BeanUtils.java new file mode 100644 index 0000000..de91374 --- /dev/null +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/utils/BeanUtils.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 io.openmessaging.rocketmq.utils; + +import io.openmessaging.KeyValue; +import java.lang.reflect.InvocationTargetException; +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.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public final class BeanUtils { + 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<>(); + + static { + primitiveWrapperMap.put(Boolean.TYPE, Boolean.class); + primitiveWrapperMap.put(Byte.TYPE, Byte.class); + primitiveWrapperMap.put(Character.TYPE, Character.class); + primitiveWrapperMap.put(Short.TYPE, Short.class); + primitiveWrapperMap.put(Integer.TYPE, Integer.class); + primitiveWrapperMap.put(Long.TYPE, Long.class); + primitiveWrapperMap.put(Double.TYPE, Double.class); + primitiveWrapperMap.put(Float.TYPE, Float.class); + primitiveWrapperMap.put(Void.TYPE, Void.TYPE); + } + + private static Map, Class> wrapperMap = new HashMap<>(); + + static { + 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); + } + + /** + *

    Populate the JavaBeans properties of the specified bean, based on + * the specified name/value pairs. This method uses Java reflection APIs + * to identify corresponding "property setter" method names, and deals + * with setter arguments of type String, boolean, + * int, long, float, and + * double.

    + * + *

    The particular setter method to be called for each property is + * determined using the usual JavaBeans introspection mechanisms. Thus, + * you may identify custom setter methods using a BeanInfo class that is + * associated with the class of the bean itself. If no such BeanInfo + * class is available, the standard method name conversion ("set" plus + * the capitalized name of the property in question) is used.

    + * + *

    NOTE: It is contrary to the JavaBeans Specification + * to have more than one setter method (with different argument + * signatures) for the same property.

    + * + * @param clazz JavaBean class whose properties are being populated + * @param properties Map keyed by property name, with the corresponding (String or String[]) value(s) to be set + * @param Class type + * @return Class instance + */ + public static T populate(final Properties properties, final Class clazz) { + T obj = null; + try { + obj = clazz.getDeclaredConstructor().newInstance(); + return populate(properties, obj); + } catch (Throwable e) { + log.warn("Error occurs !", e); + } + return obj; + } + + public static T populate(final KeyValue properties, final Class clazz) { + T obj = null; + try { + obj = clazz.getDeclaredConstructor().newInstance(); + return populate(properties, obj); + } catch (Throwable e) { + log.warn("Error occurs !", e); + } + return obj; + } + + public static Class getMethodClass(Class clazz, String methodName) { + Method[] methods = clazz.getMethods(); + for (Method method : methods) { + if (method.getName().equalsIgnoreCase(methodName)) { + return method.getParameterTypes()[0]; + } + } + return null; + } + + public static void setProperties(Class clazz, Object obj, String methodName, + Object value) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Class parameterClass = getMethodClass(clazz, methodName); + Method setterMethod = clazz.getMethod(methodName, parameterClass); + if (parameterClass == Boolean.TYPE) { + setterMethod.invoke(obj, Boolean.valueOf(value.toString())); + } else if (parameterClass == Integer.TYPE) { + setterMethod.invoke(obj, Integer.valueOf(value.toString())); + } else if (parameterClass == Double.TYPE) { + setterMethod.invoke(obj, Double.valueOf(value.toString())); + } else if (parameterClass == Float.TYPE) { + setterMethod.invoke(obj, Float.valueOf(value.toString())); + } else if (parameterClass == Long.TYPE) { + setterMethod.invoke(obj, Long.valueOf(value.toString())); + } else + setterMethod.invoke(obj, value); + } + + public static T populate(final Properties properties, final T obj) { + Class clazz = obj.getClass(); + try { + + Set> entries = properties.entrySet(); + for (Map.Entry entry : entries) { + String entryKey = entry.getKey().toString(); + String[] keyGroup = entryKey.split("\\."); + for (int i = 0; i < keyGroup.length; i++) { + keyGroup[i] = keyGroup[i].toLowerCase(); + keyGroup[i] = StringUtils.capitalize(keyGroup[i]); + } + String beanFieldNameWithCapitalization = StringUtils.join(keyGroup); + try { + setProperties(clazz, obj, "set" + beanFieldNameWithCapitalization, entry.getValue()); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ignored) { + //ignored... + } + } + } catch (RuntimeException e) { + log.warn("Error occurs !", e); + } + return obj; + } + + public static T populate(final KeyValue properties, final T obj) { + Class clazz = obj.getClass(); + try { + + final Set keySet = properties.keySet(); + for (String key : keySet) { + String[] keyGroup = key.split("[\\._]"); + for (int i = 0; i < keyGroup.length; i++) { + keyGroup[i] = keyGroup[i].toLowerCase(); + keyGroup[i] = StringUtils.capitalize(keyGroup[i]); + } + String beanFieldNameWithCapitalization = StringUtils.join(keyGroup); + try { + setProperties(clazz, obj, "set" + beanFieldNameWithCapitalization, properties.getString(key)); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ignored) { + //ignored... + } + } + } catch (RuntimeException e) { + log.warn("Error occurs !", e); + } + return obj; + } +} + diff --git a/openmessaging/src/main/java/io/openmessaging/rocketmq/utils/OMSUtil.java b/openmessaging/src/main/java/io/openmessaging/rocketmq/utils/OMSUtil.java new file mode 100644 index 0000000..66af8ce --- /dev/null +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/utils/OMSUtil.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 io.openmessaging.rocketmq.utils; + +import io.openmessaging.BytesMessage; +import io.openmessaging.KeyValue; +import io.openmessaging.Message.BuiltinKeys; +import io.openmessaging.OMS; +import io.openmessaging.producer.SendResult; +import io.openmessaging.rocketmq.domain.BytesMessageImpl; +import io.openmessaging.rocketmq.domain.RocketMQConstants; +import io.openmessaging.rocketmq.domain.SendResultImpl; +import java.lang.reflect.Field; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageAccessor; + +public class OMSUtil { + + /** + * Builds a OMS client instance name. + * + * @return a unique instance name + */ + public static String buildInstanceName() { + return Integer.toString(UtilAll.getPid()) + "%OpenMessaging" + "%" + System.nanoTime(); + } + + 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(byte[].class)); + + KeyValue sysHeaders = omsMessage.sysHeaders(); + KeyValue userHeaders = omsMessage.userHeaders(); + + //All destinations in RocketMQ use Topic + 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 : userHeaders.keySet()) { + MessageAccessor.putProperty(rmqMessage, key, userHeaders.getString(key)); + } + + //System headers has a high priority + for (String key : sysHeaders.keySet()) { + MessageAccessor.putProperty(rmqMessage, key, sysHeaders.getString(key)); + } + + return rmqMessage; + } + + public static BytesMessage msgConvert(org.apache.rocketmq.common.message.MessageExt rmqMsg) { + BytesMessage omsMsg = new BytesMessageImpl(); + omsMsg.setBody(rmqMsg.getBody()); + + KeyValue headers = omsMsg.sysHeaders(); + KeyValue properties = omsMsg.userHeaders(); + + final Set> entries = rmqMsg.getProperties().entrySet(); + + for (final Map.Entry entry : entries) { + if (isOMSHeader(entry.getKey())) { + headers.put(entry.getKey(), entry.getValue()); + } else { + properties.put(entry.getKey(), entry.getValue()); + } + } + + 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 : BuiltinKeys.class.getDeclaredFields()) { + try { + if (field.get(BuiltinKeys.class).equals(value)) { + return true; + } + } catch (IllegalAccessException e) { + return false; + } + } + return false; + } + + /** + * Convert a RocketMQ SEND_OK SendResult instance to a OMS SendResult. + */ + public static SendResult sendResultConvert(org.apache.rocketmq.client.producer.SendResult rmqResult) { + assert rmqResult.getSendStatus().equals(SendStatus.SEND_OK); + return new SendResultImpl(rmqResult.getMsgId(), OMS.newKeyValue()); + } + + public static KeyValue buildKeyValue(KeyValue... keyValues) { + KeyValue keyValue = OMS.newKeyValue(); + for (KeyValue properties : keyValues) { + for (String key : properties.keySet()) { + keyValue.put(key, properties.getString(key)); + } + } + return keyValue; + } + + /** + * Returns an iterator that cycles indefinitely over the elements of {@code Iterable}. + */ + public static Iterator cycle(final Iterable iterable) { + return new Iterator() { + Iterator iterator = new Iterator() { + @Override + public synchronized boolean hasNext() { + return false; + } + + @Override + public synchronized T next() { + throw new NoSuchElementException(); + } + + @Override + public synchronized void remove() { + //Ignore + } + }; + + @Override + public synchronized boolean hasNext() { + return iterator.hasNext() || iterable.iterator().hasNext(); + } + + @Override + public synchronized T next() { + if (!iterator.hasNext()) { + iterator = iterable.iterator(); + if (!iterator.hasNext()) { + throw new NoSuchElementException(); + } + } + return iterator.next(); + } + + @Override + public synchronized void remove() { + iterator.remove(); + } + }; + } +} diff --git a/openmessaging/src/test/java/io/openmessaging/rocketmq/consumer/LocalMessageCacheTest.java b/openmessaging/src/test/java/io/openmessaging/rocketmq/consumer/LocalMessageCacheTest.java new file mode 100644 index 0000000..851c283 --- /dev/null +++ b/openmessaging/src/test/java/io/openmessaging/rocketmq/consumer/LocalMessageCacheTest.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 io.openmessaging.rocketmq.consumer; + +import io.openmessaging.rocketmq.config.ClientConfig; +import io.openmessaging.rocketmq.domain.ConsumeRequest; +import io.openmessaging.rocketmq.domain.NonStandardKeys; +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +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.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class LocalMessageCacheTest { + private LocalMessageCache localMessageCache; + @Mock + private DefaultMQPullConsumer rocketmqPullConsume; + @Mock + private ConsumeRequest consumeRequest; + + @Before + public void init() { + ClientConfig clientConfig = new ClientConfig(); + clientConfig.setRmqPullMessageBatchNums(512); + clientConfig.setRmqPullMessageCacheCapacity(1024); + localMessageCache = new LocalMessageCache(rocketmqPullConsume, clientConfig); + } + + @Test + public void testNextPullBatchNums() throws Exception { + assertThat(localMessageCache.nextPullBatchNums()).isEqualTo(512); + for (int i = 0; i < 513; i++) { + localMessageCache.submitConsumeRequest(consumeRequest); + } + assertThat(localMessageCache.nextPullBatchNums()).isEqualTo(511); + } + + @Test + public void testNextPullOffset() throws Exception { + MessageQueue messageQueue = new MessageQueue(); + when(rocketmqPullConsume.fetchConsumeOffset(any(MessageQueue.class), anyBoolean())) + .thenReturn(123L); + assertThat(localMessageCache.nextPullOffset(new MessageQueue())).isEqualTo(123L); + } + + @Test + public void testUpdatePullOffset() throws Exception { + MessageQueue messageQueue = new MessageQueue(); + localMessageCache.updatePullOffset(messageQueue, 124L); + assertThat(localMessageCache.nextPullOffset(messageQueue)).isEqualTo(124L); + } + + @Test + public void testSubmitConsumeRequest() throws Exception { + byte[] body = new byte[] {'1', '2', '3'}; + MessageExt consumedMsg = new MessageExt(); + consumedMsg.setMsgId("NewMsgId"); + consumedMsg.setBody(body); + consumedMsg.putUserProperty(NonStandardKeys.MESSAGE_DESTINATION, "TOPIC"); + consumedMsg.setTopic("HELLO_QUEUE"); + + when(consumeRequest.getMessageExt()).thenReturn(consumedMsg); + localMessageCache.submitConsumeRequest(consumeRequest); + assertThat(localMessageCache.poll()).isEqualTo(consumedMsg); + } +} \ No newline at end of file diff --git a/openmessaging/src/test/java/io/openmessaging/rocketmq/consumer/PullConsumerImplTest.java b/openmessaging/src/test/java/io/openmessaging/rocketmq/consumer/PullConsumerImplTest.java new file mode 100644 index 0000000..5a0fd9c --- /dev/null +++ b/openmessaging/src/test/java/io/openmessaging/rocketmq/consumer/PullConsumerImplTest.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 io.openmessaging.rocketmq.consumer; + +import io.openmessaging.BytesMessage; +import io.openmessaging.Message; +import io.openmessaging.MessagingAccessPoint; +import io.openmessaging.OMS; +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; +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.common.message.MessageExt; +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.spy; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class PullConsumerImplTest { + private PullConsumer consumer; + private String queueName = "HELLO_QUEUE"; + + @Mock + private DefaultMQPullConsumer rocketmqPullConsumer; + private LocalMessageCache localMessageCache = null; + + @Before + public void init() throws NoSuchFieldException, IllegalAccessException { + final MessagingAccessPoint messagingAccessPoint = OMS + .getMessagingAccessPoint("oms:rocketmq://IP1:9876,IP2:9876/namespace"); + + 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.setOperationTimeout(200); + localMessageCache = spy(new LocalMessageCache(rocketmqPullConsumer, clientConfig)); + + field = PullConsumerImpl.class.getDeclaredField("localMessageCache"); + field.setAccessible(true); + field.set(consumer, localMessageCache); + + messagingAccessPoint.startup(); + consumer.startup(); + } + + @Test + public void testPoll() { + final byte[] testBody = new byte[] {'a', 'b'}; + MessageExt consumedMsg = new MessageExt(); + consumedMsg.setMsgId("NewMsgId"); + consumedMsg.setBody(testBody); + consumedMsg.putUserProperty(NonStandardKeys.MESSAGE_DESTINATION, "TOPIC"); + consumedMsg.setTopic(queueName); + + when(localMessageCache.poll()).thenReturn(consumedMsg); + + 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.receive(); + assertThat(message).isNull(); + + 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 new file mode 100644 index 0000000..d80e026 --- /dev/null +++ b/openmessaging/src/test/java/io/openmessaging/rocketmq/consumer/PushConsumerImplTest.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 io.openmessaging.rocketmq.consumer; + +import io.openmessaging.BytesMessage; +import io.openmessaging.Message; +import io.openmessaging.OMSBuiltinKeys; +import io.openmessaging.consumer.MessageListener; +import io.openmessaging.MessagingAccessPoint; +import io.openmessaging.OMS; +import io.openmessaging.consumer.PushConsumer; +import io.openmessaging.rocketmq.domain.NonStandardKeys; +import java.lang.reflect.Field; +import java.util.Collections; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.common.message.MessageExt; +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 PushConsumerImplTest { + private PushConsumer consumer; + + @Mock + private DefaultMQPushConsumer rocketmqPushConsumer; + + @Before + public void init() throws NoSuchFieldException, IllegalAccessException { + final MessagingAccessPoint messagingAccessPoint = OMS + .getMessagingAccessPoint("oms:rocketmq://IP1:9876,IP2:9876/namespace"); + consumer = messagingAccessPoint.createPushConsumer( + OMS.newKeyValue().put(OMSBuiltinKeys.CONSUMER_ID, "TestGroup")); + + Field field = PushConsumerImpl.class.getDeclaredField("rocketmqPushConsumer"); + field.setAccessible(true); + DefaultMQPushConsumer innerConsumer = (DefaultMQPushConsumer) field.get(consumer); + field.set(consumer, rocketmqPushConsumer); //Replace + + when(rocketmqPushConsumer.getMessageListener()).thenReturn(innerConsumer.getMessageListener()); + messagingAccessPoint.startup(); + consumer.startup(); + } + + @Test + public void testConsumeMessage() { + final byte[] testBody = new byte[] {'a', 'b'}; + + MessageExt consumedMsg = new MessageExt(); + consumedMsg.setMsgId("NewMsgId"); + consumedMsg.setBody(testBody); + consumedMsg.putUserProperty(NonStandardKeys.MESSAGE_DESTINATION, "TOPIC"); + consumedMsg.setTopic("HELLO_QUEUE"); + consumer.attachQueue("HELLO_QUEUE", new MessageListener() { + @Override + 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(); + } + }); + ((MessageListenerConcurrently) rocketmqPushConsumer + .getMessageListener()).consumeMessage(Collections.singletonList(consumedMsg), null); + } +} \ No newline at end of file diff --git a/openmessaging/src/test/java/io/openmessaging/rocketmq/producer/ProducerImplTest.java b/openmessaging/src/test/java/io/openmessaging/rocketmq/producer/ProducerImplTest.java new file mode 100644 index 0000000..fc6515e --- /dev/null +++ b/openmessaging/src/test/java/io/openmessaging/rocketmq/producer/ProducerImplTest.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 io.openmessaging.rocketmq.producer; + +import io.openmessaging.MessagingAccessPoint; +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; +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.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Fail.failBecauseExceptionWasNotThrown; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ProducerImplTest { + private Producer producer; + + @Mock + private DefaultMQProducer rocketmqProducer; + + @Before + public void init() throws NoSuchFieldException, IllegalAccessException { + final MessagingAccessPoint messagingAccessPoint = OMS + .getMessagingAccessPoint("oms:rocketmq://IP1:9876,IP2:9876/namespace"); + producer = messagingAccessPoint.createProducer(); + + Field field = AbstractOMSProducer.class.getDeclaredField("rocketmqProducer"); + field.setAccessible(true); + field.set(producer, rocketmqProducer); + + messagingAccessPoint.startup(); + producer.startup(); + } + + @Test + public void testSend_OK() throws InterruptedException, RemotingException, MQClientException, MQBrokerException { + SendResult sendResult = new SendResult(); + sendResult.setMsgId("TestMsgID"); + sendResult.setSendStatus(SendStatus.SEND_OK); + when(rocketmqProducer.send(any(Message.class), anyLong())).thenReturn(sendResult); + io.openmessaging.producer.SendResult omsResult = + producer.send(producer.createBytesMessage("HELLO_TOPIC", new byte[] {'a'})); + + assertThat(omsResult.messageId()).isEqualTo("TestMsgID"); + } + + @Test + public void testSend_Not_OK() throws InterruptedException, RemotingException, MQClientException, MQBrokerException { + SendResult sendResult = new SendResult(); + sendResult.setSendStatus(SendStatus.FLUSH_DISK_TIMEOUT); + + when(rocketmqProducer.send(any(Message.class), anyLong())).thenReturn(sendResult); + try { + producer.send(producer.createBytesMessage("HELLO_TOPIC", new byte[] {'a'})); + failBecauseExceptionWasNotThrown(OMSRuntimeException.class); + } catch (Exception e) { + assertThat(e).hasMessageContaining("Send message to RocketMQ broker failed."); + } + } + + @Test + public void testSend_WithException() throws InterruptedException, RemotingException, MQClientException, MQBrokerException { + when(rocketmqProducer.send(any(Message.class), anyLong())).thenThrow(MQClientException.class); + try { + producer.send(producer.createBytesMessage("HELLO_TOPIC", new byte[] {'a'})); + failBecauseExceptionWasNotThrown(OMSRuntimeException.class); + } catch (Exception e) { + assertThat(e).hasMessageContaining("Send message to RocketMQ broker failed."); + } + } + +} \ 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 new file mode 100644 index 0000000..f226ede --- /dev/null +++ b/openmessaging/src/test/java/io/openmessaging/rocketmq/promise/DefaultPromiseTest.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 io.openmessaging.rocketmq.promise; + +import io.openmessaging.Future; +import io.openmessaging.FutureListener; +import io.openmessaging.Promise; +import io.openmessaging.exception.OMSRuntimeException; +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Fail.failBecauseExceptionWasNotThrown; + +public class DefaultPromiseTest { + private Promise promise; + + @Before + public void init() { + promise = new DefaultPromise<>(); + } + + @Test + public void testIsCancelled() throws Exception { + assertThat(promise.isCancelled()).isEqualTo(false); + } + + @Test + public void testIsDone() throws Exception { + assertThat(promise.isDone()).isEqualTo(false); + promise.set("Done"); + assertThat(promise.isDone()).isEqualTo(true); + } + + @Test + public void testGet() throws Exception { + promise.set("Done"); + assertThat(promise.get()).isEqualTo("Done"); + } + + @Test + public void testGet_WithTimeout() throws Exception { + try { + promise.get(100); + failBecauseExceptionWasNotThrown(OMSRuntimeException.class); + } catch (OMSRuntimeException e) { + assertThat(e).hasMessageContaining("Get request result is timeout or interrupted"); + } + } + + @Test + public void testAddListener() throws Exception { + promise.addListener(new FutureListener() { + @Override + public void operationComplete(Future future) { + assertThat(promise.get()).isEqualTo("Done"); + + } + }); + promise.set("Done"); + } + + @Test + public void testAddListener_ListenerAfterSet() throws Exception { + promise.set("Done"); + promise.addListener(new FutureListener() { + @Override + public void operationComplete(Future future) { + assertThat(future.get()).isEqualTo("Done"); + } + }); + } + + @Test + public void testAddListener_WithException_ListenerAfterSet() throws Exception { + final Throwable exception = new OMSRuntimeException("-1", "Test Error"); + promise.setFailure(exception); + promise.addListener(new FutureListener() { + @Override + public void operationComplete(Future future) { + assertThat(promise.getThrowable()).isEqualTo(exception); + } + }); + } + + @Test + public void testAddListener_WithException() throws Exception { + final Throwable exception = new OMSRuntimeException("-1", "Test Error"); + promise.addListener(new FutureListener() { + @Override + public void operationComplete(Future future) { + assertThat(promise.getThrowable()).isEqualTo(exception); + } + }); + promise.setFailure(exception); + } + + @Test + public void getThrowable() throws Exception { + assertThat(promise.getThrowable()).isNull(); + Throwable exception = new OMSRuntimeException("-1", "Test Error"); + promise.setFailure(exception); + assertThat(promise.getThrowable()).isEqualTo(exception); + } + +} \ No newline at end of file diff --git a/openmessaging/src/test/java/io/openmessaging/rocketmq/utils/BeanUtilsTest.java b/openmessaging/src/test/java/io/openmessaging/rocketmq/utils/BeanUtilsTest.java new file mode 100644 index 0000000..1a431d9 --- /dev/null +++ b/openmessaging/src/test/java/io/openmessaging/rocketmq/utils/BeanUtilsTest.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 io.openmessaging.rocketmq.utils; + +import io.openmessaging.KeyValue; +import io.openmessaging.OMS; +import io.openmessaging.rocketmq.config.ClientConfig; +import io.openmessaging.rocketmq.domain.NonStandardKeys; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class BeanUtilsTest { + private KeyValue properties = OMS.newKeyValue(); + + public static class CustomizedConfig extends ClientConfig { + final static String STRING_TEST = "string.test"; + String stringTest = "foobar"; + + final static String DOUBLE_TEST = "double.test"; + double doubleTest = 123.0; + + final static String LONG_TEST = "long.test"; + long longTest = 123L; + + String getStringTest() { + return stringTest; + } + + public void setStringTest(String stringTest) { + this.stringTest = stringTest; + } + + double getDoubleTest() { + return doubleTest; + } + + public void setDoubleTest(final double doubleTest) { + this.doubleTest = doubleTest; + } + + long getLongTest() { + return longTest; + } + + public void setLongTest(final long longTest) { + this.longTest = longTest; + } + + CustomizedConfig() { + } + } + + @Before + public void init() { + properties.put(NonStandardKeys.MAX_REDELIVERY_TIMES, 120); + properties.put(CustomizedConfig.STRING_TEST, "kaka"); + properties.put(NonStandardKeys.CONSUMER_GROUP, "Default_Consumer_Group"); + properties.put(NonStandardKeys.MESSAGE_CONSUME_TIMEOUT, 101); + + properties.put(CustomizedConfig.LONG_TEST, 1234567890L); + properties.put(CustomizedConfig.DOUBLE_TEST, 10.234); + } + + @Test + public void testPopulate() { + CustomizedConfig config = BeanUtils.populate(properties, CustomizedConfig.class); + + //RemotingConfig config = BeanUtils.populate(properties, RemotingConfig.class); + Assert.assertEquals(config.getRmqMaxRedeliveryTimes(), 120); + Assert.assertEquals(config.getStringTest(), "kaka"); + Assert.assertEquals(config.getRmqConsumerGroup(), "Default_Consumer_Group"); + Assert.assertEquals(config.getRmqMessageConsumeTimeout(), 101); + Assert.assertEquals(config.getLongTest(), 1234567890L); + Assert.assertEquals(config.getDoubleTest(), 10.234, 0.000001); + } + + @Test + public void testPopulate_ExistObj() { + CustomizedConfig config = new CustomizedConfig(); + config.setConsumerId("NewConsumerId"); + + Assert.assertEquals(config.getConsumerId(), "NewConsumerId"); + + config = BeanUtils.populate(properties, config); + + //RemotingConfig config = BeanUtils.populate(properties, RemotingConfig.class); + Assert.assertEquals(config.getRmqMaxRedeliveryTimes(), 120); + Assert.assertEquals(config.getStringTest(), "kaka"); + Assert.assertEquals(config.getRmqConsumerGroup(), "Default_Consumer_Group"); + Assert.assertEquals(config.getRmqMessageConsumeTimeout(), 101); + Assert.assertEquals(config.getLongTest(), 1234567890L); + Assert.assertEquals(config.getDoubleTest(), 10.234, 0.000001); + } + +} \ No newline at end of file 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 0000000..8695d52 --- /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 new file mode 100644 index 0000000..003e29e --- /dev/null +++ b/pom.xml @@ -0,0 +1,1152 @@ + + + + + + org.apache + apache + 18 + + + 4.0.0 + + 2012 + org.apache.rocketmq + rocketmq-all + 5.3.4-SNAPSHOT + pom + Apache RocketMQ ${project.version} + http://rocketmq.apache.org/ + + + git@github.com:apache/rocketmq.git + scm:git:git@github.com:apache/rocketmq.git + scm:git:git@github.com:apache/rocketmq.git + HEAD + + + + + Development List + dev-subscribe@rocketmq.apache.org + dev-unsubscribe@rocketmq.apache.org + dev@rocketmq.apache.org + + + User List + users-subscribe@rocketmq.apache.org + users-unsubscribe@rocketmq.apache.org + users@rocketmq.apache.org + + + Commits List + commits-subscribe@rocketmq.apache.org + commits-unsubscribe@rocketmq.apache.org + commits@rocketmq.apache.org + + + + + + Apache RocketMQ + Apache RocketMQ of ASF + https://rocketmq.apache.org/ + + + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0 + repo + + + + + Apache Software Foundation + http://www.apache.org + + + + jira + https://issues.apache.org/jira/browse/RocketMQ + + + + UTF-8 + UTF-8 + ${basedir} + + + false + true + + 1.8 + 1.8 + + 1.5.0 + 4.1.119.Final + 2.0.53.Final + 1.69 + 1.2.83 + 2.0.43 + 3.20.0-GA + 4.2.2 + 3.12.0 + 2.14.0 + 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.8.1 + 0.3.1.2 + 6.0.53 + 1.0-beta-4 + 1.4.2 + 2.0.4 + 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 + 4.11.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/** + + + + client + common + broker + tools + store + namesrv + remoting + srvutil + filter + test + distribution + openmessaging + auth + example + container + controller + proxy + tieredstore + + + + + + org.codehaus.mojo + versions-maven-plugin + ${versions-maven-plugin.version} + + + com.github.vongosling + dependency-mediator-maven-plugin + ${dependency-mediator-maven-plugin.version} + + + org.codehaus.mojo + clirr-maven-plugin + ${clirr-maven-plugin.version} + + + maven-enforcer-plugin + ${maven-enforcer-plugin.version} + + + enforce-ban-circular-dependencies + + enforce + + + + + + + + + true + + + + org.codehaus.mojo + extra-enforcer-rules + ${extra-enforcer-rules.version} + + + + + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.source} + true + true + + + + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar + + + + + + maven-help-plugin + ${maven-help-plugin.version} + + + generate-effective-dependencies-pom + generate-resources + + ${project.build.directory}/effective-pom/effective-dependencies.xml + + + + + + maven-checkstyle-plugin + ${maven-checkstyle-plugin.version} + + + validate + validate + + style/rmq_checkstyle.xml + UTF-8 + true + true + true + **/generated*/**/* + + + check + + + + + + org.apache.rat + apache-rat-plugin + ${apache-rat-plugin.version} + + + .gitignore + .travis.yml + README.md + CONTRIBUTING.md + bin/README.md + .github/** + src/test/resources/** + src/test/resources/certs/* + src/test/**/*.log + src/test/resources/META-INF/services/* + src/main/resources/META-INF/services/* + */target/** + */*.iml + docs/** + localbin/** + conf/rmq-proxy.json + .bazelversion + + + + + maven-resources-plugin + ${maven-resources-plugin.version} + + + ${project.build.sourceEncoding} + + + + org.eluder.coveralls + coveralls-maven-plugin + ${coveralls-maven-plugin.version} + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + default-prepare-agent + + prepare-agent + + + ${project.build.directory}/jacoco.exec + + + + report + test + + report + + + + default-prepare-agent-integration + pre-integration-test + + prepare-agent-integration + + + ${project.build.directory}/jacoco-it.exec + failsafeArgLine + + + + default-report + + report + + + + default-report-integration + + report-integration + + + + + + maven-surefire-plugin + ${maven-surefire-plugin.version} + + 1 + 1 + true + + **/IT*.java + + + + + org.sonarsource.scanner.maven + sonar-maven-plugin + ${sonar-maven-plugin.version} + + + com.github.spotbugs + spotbugs-maven-plugin + ${spotbugs-plugin.version} + + + check + compile + + check + + + + + true + false + true + ${project.root}/style/spotbugs-suppressions.xml + High + Max + + + + + + + + maven-assembly-plugin + ${maven-assembly-plugin.version} + + + + + + + + jdk8 + + [1.8,) + + + + + + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + -Xdoclint:none + + + + + + + + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + -Xdoclint:none + + + + + + + release-sign-artifacts + + + performRelease + true + + + + + + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + + + + + + it-test + + + + maven-failsafe-plugin + ${maven-failsafe-plugin.version} + + @{failsafeArgLine} + + **/NormalMsgDelayIT.java + + + + + + integration-test + verify + + + + + + + + + sonar-apache + + + https://builds.apache.org/analysis + + + + skip-unit-tests + + + + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + true + + + + + + + + + + + org.apache.rocketmq + rocketmq-auth + ${project.version} + + + org.apache.rocketmq + rocketmq-broker + ${project.version} + + + org.apache.rocketmq + rocketmq-client + ${project.version} + + + org.apache.rocketmq + rocketmq-common + ${project.version} + + + org.apache.rocketmq + rocketmq-container + ${project.version} + + + org.apache.rocketmq + rocketmq-controller + ${project.version} + + + org.apache.rocketmq + rocketmq-example + ${project.version} + + + org.apache.rocketmq + rocketmq-filter + ${project.version} + + + org.apache.rocketmq + rocketmq-namesrv + ${project.version} + + + org.apache.rocketmq + rocketmq-openmessaging + ${project.version} + + + 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-store + ${project.version} + + + org.apache.rocketmq + rocketmq-tiered-store + ${project.version} + + + org.apache.rocketmq + rocketmq-test + ${project.version} + + + org.apache.rocketmq + rocketmq-tools + ${project.version} + + + ${project.groupId} + rocketmq-proto + ${rocketmq-proto.version} + + + * + * + + + + + commons-cli + commons-cli + ${commons-cli.version} + + + io.netty + netty-all + ${netty.version} + + + org.bouncycastle + bcpkix-jdk15on + runtime + jar + ${bcpkix-jdk15on.version} + + + com.alibaba + fastjson + ${fastjson.version} + + + com.alibaba.fastjson2 + fastjson2 + ${fastjson2.version} + + + org.javassist + javassist + ${javassist.version} + + + net.java.dev.jna + jna + ${jna.version} + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + commons-io + commons-io + ${commons-io.version} + + + com.google.guava + guava + ${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 + ${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 + + + org.mockito + mockito-junit-jupiter + ${mockito-junit-jupiter.version} + test + + + org.awaitility + awaitility + ${awaitility.version} + + + com.google.truth + truth + ${truth.version} + + + com.google.errorprone + error_prone_annotations + + + + + + 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 + + + + + + jakarta.annotation + jakarta.annotation-api + 1.3.5 + + + + + + + junit + junit + ${junit.version} + test + + + org.assertj + assertj-core + ${assertj-core.version} + test + + + org.mockito + mockito-core + ${mockito-core.version} + test + + + org.mockito + mockito-junit-jupiter + ${mockito-junit-jupiter.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 0000000..b996e8b --- /dev/null +++ b/proxy/BUILD.bazel @@ -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. +# +load("//bazel:GenTestRules.bzl", "GenTestRules") + +java_library( + name = "proxy", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//auth", + "//broker", + "//client", + "//common", + "//remoting", + "//srvutil", + "@maven//:ch_qos_logback_logback_classic", + "@maven//:ch_qos_logback_logback_core", + "@maven//:com_alibaba_fastjson", + "@maven//:com_alibaba_fastjson2_fastjson2", + "@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 = [ + "//auth", + ":proxy", + "//:test_deps", + "//broker", + "//client", + "//common", + "//remoting", + "@maven//:ch_qos_logback_logback_core", + "@maven//:com_alibaba_fastjson", + "@maven//:com_alibaba_fastjson2_fastjson2", + "@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 0000000..936bd02 --- /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 0000000..749c01c --- /dev/null +++ b/proxy/pom.xml @@ -0,0 +1,118 @@ + + + + + + rocketmq-all + org.apache.rocketmq + 5.3.4-SNAPSHOT + + + 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-auth + + + 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 0000000..0499f26 --- /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 0000000..3cc3642 --- /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 0000000..e37ba97 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/ProxyStartup.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.proxy; + +import com.google.common.collect.Lists; +import io.grpc.protobuf.services.ChannelzService; +import io.grpc.protobuf.services.ProtoReflectionService; +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.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.AbstractStartAndShutdown; +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.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; + +import java.util.Date; +import java.util.List; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +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(); + + // create grpcServer + GrpcServer grpcServer = GrpcServerBuilder.newBuilder(executor, ConfigurationManager.getProxyConfig().getGrpcServerPort()) + .addService(createServiceProcessor(messagingProcessor)) + .addService(ChannelzService.newInstance(100)) + .addService(ProtoReflectionService.newInstance()) + .configInterceptor() + .shutdownTime(ConfigurationManager.getProxyConfig().getGrpcShutdownTimeSeconds(), TimeUnit.SECONDS) + .build(); + PROXY_START_AND_SHUTDOWN.appendStartAndShutdown(grpcServer); + + RemotingProtocolServer remotingServer = new RemotingProtocolServer(messagingProcessor); + 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 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/auth/ProxyAuthenticationMetadataProvider.java b/proxy/src/main/java/org/apache/rocketmq/proxy/auth/ProxyAuthenticationMetadataProvider.java new file mode 100644 index 0000000..dd084fa --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/auth/ProxyAuthenticationMetadataProvider.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.auth; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import org.apache.rocketmq.auth.authentication.provider.AuthenticationMetadataProvider; +import org.apache.rocketmq.auth.authentication.model.User; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.proxy.service.metadata.MetadataService; + +public class ProxyAuthenticationMetadataProvider implements AuthenticationMetadataProvider { + + protected AuthConfig authConfig; + protected MetadataService metadataService; + + @Override + public void initialize(AuthConfig authConfig, Supplier metadataService) { + this.authConfig = authConfig; + if (metadataService != null) { + this.metadataService = (MetadataService) metadataService.get(); + } + } + + @Override + public void shutdown() { + + } + + @Override + public CompletableFuture createUser(User user) { + return null; + } + + @Override + public CompletableFuture deleteUser(String username) { + return null; + } + + @Override + public CompletableFuture updateUser(User user) { + return null; + } + + @Override + public CompletableFuture getUser(String username) { + return this.metadataService.getUser(null, username); + } + + @Override + public CompletableFuture> listUser(String filter) { + return null; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/auth/ProxyAuthorizationMetadataProvider.java b/proxy/src/main/java/org/apache/rocketmq/proxy/auth/ProxyAuthorizationMetadataProvider.java new file mode 100644 index 0000000..54fa7e4 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/auth/ProxyAuthorizationMetadataProvider.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.auth; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import org.apache.rocketmq.auth.authentication.model.Subject; +import org.apache.rocketmq.auth.authorization.provider.AuthorizationMetadataProvider; +import org.apache.rocketmq.auth.authorization.model.Acl; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.proxy.service.metadata.MetadataService; + +public class ProxyAuthorizationMetadataProvider implements AuthorizationMetadataProvider { + + protected AuthConfig authConfig; + + protected MetadataService metadataService; + + @Override + public void initialize(AuthConfig authConfig, Supplier metadataService) { + this.authConfig = authConfig; + if (metadataService != null) { + this.metadataService = (MetadataService) metadataService.get(); + } + } + + @Override + public void shutdown() { + + } + + @Override + public CompletableFuture createAcl(Acl acl) { + return null; + } + + @Override + public CompletableFuture deleteAcl(Subject subject) { + return null; + } + + @Override + public CompletableFuture updateAcl(Acl acl) { + return null; + } + + @Override + public CompletableFuture getAcl(Subject subject) { + return this.metadataService.getAcl(null, subject); + } + + @Override + public CompletableFuture> listAcl(String subjectFilter, String resourceFilter) { + return null; + } +} 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 0000000..581caff --- /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 0000000..1f24719 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/common/Address.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.common; + +import com.google.common.net.HostAndPort; +import java.util.Objects; +import org.apache.rocketmq.common.utils.IPAddressUtils; + +public class Address { + + public enum AddressScheme { + IPv4, + IPv6, + DOMAIN_NAME, + UNRECOGNIZED + } + + private AddressScheme addressScheme; + private HostAndPort hostAndPort; + + public Address(HostAndPort hostAndPort) { + this.addressScheme = buildScheme(hostAndPort); + this.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; + } + + private AddressScheme buildScheme(HostAndPort hostAndPort) { + if (hostAndPort == null) { + return AddressScheme.UNRECOGNIZED; + } + String address = hostAndPort.getHost(); + if (IPAddressUtils.isValidIPv4(address)) { + return AddressScheme.IPv4; + } + if (IPAddressUtils.isValidIPv6(address)) { + return AddressScheme.IPv6; + } + return AddressScheme.DOMAIN_NAME; + } + + @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 0000000..93b4eac --- /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 0000000..c015e9f --- /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 0000000..e6fc989 --- /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 0000000..af52832 --- /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 0000000..4f91388 --- /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 0000000..15da628 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/common/ReceiptHandleGroup.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.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.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +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.proxy.config.ConfigurationManager; + +public class ReceiptHandleGroup { + protected final static Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + + // 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 final AtomicLong lastLockTimeMs = new AtomicLong(-1L); + private volatile boolean needRemove = false; + private volatile MessageReceiptHandle messageReceiptHandle; + + public HandleData(MessageReceiptHandle messageReceiptHandle) { + this.messageReceiptHandle = messageReceiptHandle; + } + + public Long lock(long timeoutMs) { + try { + boolean result = this.semaphore.tryAcquire(timeoutMs, TimeUnit.MILLISECONDS); + long currentTimeMs = System.currentTimeMillis(); + if (result) { + this.lastLockTimeMs.set(currentTimeMs); + return currentTimeMs; + } else { + // if the lock is expired, can be acquired again + long expiredTimeMs = ConfigurationManager.getProxyConfig().getLockTimeoutMsInHandleGroup() * 3; + if (currentTimeMs - this.lastLockTimeMs.get() > expiredTimeMs) { + synchronized (this) { + if (currentTimeMs - this.lastLockTimeMs.get() > expiredTimeMs) { + log.warn("HandleData lock expired, acquire lock success and reset lock time. " + + "MessageReceiptHandle={}, lockTime={}", messageReceiptHandle, currentTimeMs); + this.lastLockTimeMs.set(currentTimeMs); + return currentTimeMs; + } + } + } + } + return null; + } catch (InterruptedException e) { + return null; + } + } + + public void unlock(long lockTimeMs) { + // if the lock is expired, we don't need to unlock it + if (System.currentTimeMillis() - lockTimeMs > ConfigurationManager.getProxyConfig().getLockTimeoutMsInHandleGroup() * 2) { + log.warn("HandleData lock expired, unlock fail. MessageReceiptHandle={}, lockTime={}, now={}", + messageReceiptHandle, lockTimeMs, System.currentTimeMillis()); + return; + } + 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); + } + Long lockTimeMs = handleData.lock(timeout); + if (lockTimeMs == null) { + 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(lockTimeMs); + } + 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) -> { + Long lockTimeMs = handleData.lock(timeout); + if (lockTimeMs == null) { + 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(lockTimeMs); + } + 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) -> { + Long lockTimeMs = handleData.lock(timeout); + if (lockTimeMs == null) { + 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(lockTimeMs); + } + }); + 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) -> { + Long lockTimeMs = handleData.lock(timeout); + if (lockTimeMs == null) { + 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(lockTimeMs); + } + 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 0000000..bd28393 --- /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 0000000..8d59156 --- /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 0000000..ce33619 --- /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 0000000..dd15c85 --- /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/FilterUtils.java b/proxy/src/main/java/org/apache/rocketmq/proxy/common/utils/FilterUtils.java new file mode 100644 index 0000000..9e44ace --- /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/GrpcUtils.java b/proxy/src/main/java/org/apache/rocketmq/proxy/common/utils/GrpcUtils.java new file mode 100644 index 0000000..5c50de4 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/common/utils/GrpcUtils.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 io.grpc.Attributes; +import io.grpc.Metadata; +import io.grpc.ServerCall; + +public class GrpcUtils { + + private GrpcUtils() { + } + + public static void putHeaderIfNotExist(Metadata headers, Metadata.Key key, T value) { + if (headers == null) { + return; + } + if (!headers.containsKey(key) && value != null) { + headers.put(key, value); + } + } + + public static T getAttribute(ServerCall call, Attributes.Key key) { + Attributes attributes = call.getAttributes(); + if (attributes == null) { + return null; + } + return attributes.get(key); + } +} 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 0000000..7e82a49 --- /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 0000000..37757f8 --- /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 0000000..5b7c6c3 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/config/Configuration.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.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.auth.config.AuthConfig; +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<>(); + private final AtomicReference authConfigReference = 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); + + AuthConfig authConfig = JSON.parseObject(proxyConfigData, AuthConfig.class); + setAuthConfig(authConfig); + authConfig.setConfigName(proxyConfig.getProxyName()); + authConfig.setClusterName(proxyConfig.getRocketMQClusterName()); + } + + 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); + } + + public AuthConfig getAuthConfig() { + return authConfigReference.get(); + } + + public void setAuthConfig(AuthConfig authConfig) { + authConfigReference.set(authConfig); + } +} 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 0000000..24e1bd4 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/config/ConfigurationManager.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.config; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.serializer.SerializerFeature; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.auth.config.AuthConfig; +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 AuthConfig getAuthConfig() { + return configuration.getAuthConfig(); + } + + 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 0000000..fb53290 --- /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 0000000..e3e60b7 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/config/ProxyConfig.java @@ -0,0 +1,1531 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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; + +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; + +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; + /** + * if true, proxy will check message body size and reject msg if it's body is empty + */ + private boolean enableMessageBodyEmptyCheck = true; + /** + * 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 userCacheExpiredSeconds = 300; + private int userCacheRefreshSeconds = 20; + private int userCacheMaxNum = 20000; + private int aclCacheExpiredSeconds = 300; + private int aclCacheRefreshSeconds = 20; + private int aclCacheMaxNum = 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 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 getUserCacheExpiredSeconds() { + return userCacheExpiredSeconds; + } + + public void setUserCacheExpiredSeconds(int userCacheExpiredSeconds) { + this.userCacheExpiredSeconds = userCacheExpiredSeconds; + } + + public int getUserCacheRefreshSeconds() { + return userCacheRefreshSeconds; + } + + public void setUserCacheRefreshSeconds(int userCacheRefreshSeconds) { + this.userCacheRefreshSeconds = userCacheRefreshSeconds; + } + + public int getUserCacheMaxNum() { + return userCacheMaxNum; + } + + public void setUserCacheMaxNum(int userCacheMaxNum) { + this.userCacheMaxNum = userCacheMaxNum; + } + + public int getAclCacheExpiredSeconds() { + return aclCacheExpiredSeconds; + } + + public void setAclCacheExpiredSeconds(int aclCacheExpiredSeconds) { + this.aclCacheExpiredSeconds = aclCacheExpiredSeconds; + } + + public int getAclCacheRefreshSeconds() { + return aclCacheRefreshSeconds; + } + + public void setAclCacheRefreshSeconds(int aclCacheRefreshSeconds) { + this.aclCacheRefreshSeconds = aclCacheRefreshSeconds; + } + + public int getAclCacheMaxNum() { + return aclCacheMaxNum; + } + + public void setAclCacheMaxNum(int aclCacheMaxNum) { + this.aclCacheMaxNum = aclCacheMaxNum; + } + + 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 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; + } + + public boolean isEnableMessageBodyEmptyCheck() { + return enableMessageBodyEmptyCheck; + } + + public void setEnableMessageBodyEmptyCheck(boolean enableMessageBodyEmptyCheck) { + this.enableMessageBodyEmptyCheck = enableMessageBodyEmptyCheck; + } +} 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 0000000..d5b896f --- /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 0000000..ab00b96 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/GrpcServerBuilder.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.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 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.ContextInterceptor; +import org.apache.rocketmq.proxy.grpc.interceptor.GlobalExceptionInterceptor; +import org.apache.rocketmq.proxy.grpc.interceptor.HeaderInterceptor; + +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +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: {}, bossLoopNum: {}, workerLoopNum: {}, 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() { + 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 0000000..e0a6099 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/ProxyAndTlsProtocolNegotiator.java @@ -0,0 +1,288 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; + + private ProtocolNegotiationEvent pne = InternalProtocolNegotiationEvent.getDefault(); + + 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)); + } + + Attributes.Builder builder = InternalProtocolNegotiationEvent.getAttributes(pne).toBuilder(); + builder.set(AttributeKeys.CHANNEL_ID, ctx.channel().id().asLongText()); + + ctx.fireUserEventTriggered(InternalProtocolNegotiationEvent.withAttributes(pne, builder.build())); + ctx.pipeline().remove(this); + } catch (Exception e) { + log.error("process proxy 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); + } + } + } + + 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(); + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof ProtocolNegotiationEvent) { + pne = (ProtocolNegotiationEvent) evt; + } else { + super.userEventTriggered(ctx, evt); + } + } + } + + 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 0000000..874b398 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/constant/AttributeKeys.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.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 CHANNEL_ID = + Attributes.Key.create(HAProxyConstants.CHANNEL_ID); + + 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/ContextInterceptor.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/interceptor/ContextInterceptor.java new file mode 100644 index 0000000..112dd26 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/interceptor/ContextInterceptor.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.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; +import org.apache.rocketmq.common.constant.GrpcConstants; + +public class ContextInterceptor implements ServerInterceptor { + + @Override + public ServerCall.Listener interceptCall( + ServerCall call, + Metadata headers, + ServerCallHandler next + ) { + Context context = Context.current().withValue(GrpcConstants.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 0000000..3ce00b4 --- /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 0000000..e3e7884 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/interceptor/HeaderInterceptor.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.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.common.constant.GrpcConstants; +import org.apache.rocketmq.proxy.common.utils.GrpcUtils; +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); + } + GrpcUtils.putHeaderIfNotExist(headers, GrpcConstants.REMOTE_ADDRESS, remoteAddress); + + SocketAddress localSocketAddress = call.getAttributes().get(Grpc.TRANSPORT_ATTR_LOCAL_ADDR); + String localAddress = parseSocketAddress(localSocketAddress); + GrpcUtils.putHeaderIfNotExist(headers, GrpcConstants.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)); + GrpcUtils.putHeaderIfNotExist(headers, headerKey, headerValue); + } + + String channelId = call.getAttributes().get(AttributeKeys.CHANNEL_ID); + if (StringUtils.isNotBlank(channelId)) { + GrpcUtils.putHeaderIfNotExist(headers, GrpcConstants.CHANNEL_ID, channelId); + } + + 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/RequestMapping.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/interceptor/RequestMapping.java new file mode 100644 index 0000000..f5edc03 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/interceptor/RequestMapping.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.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.RecallMessageRequest; +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(RecallMessageRequest.getDescriptor().getFullName(), RequestCode.RECALL_MESSAGE); + 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/pipeline/AuthenticationPipeline.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/pipeline/AuthenticationPipeline.java new file mode 100644 index 0000000..e317b48 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/pipeline/AuthenticationPipeline.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.pipeline; + +import com.google.protobuf.GeneratedMessageV3; +import io.grpc.Context; +import io.grpc.Metadata; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.auth.authentication.AuthenticationEvaluator; +import org.apache.rocketmq.auth.authentication.context.AuthenticationContext; +import org.apache.rocketmq.auth.authentication.context.DefaultAuthenticationContext; +import org.apache.rocketmq.auth.authentication.exception.AuthenticationException; +import org.apache.rocketmq.auth.authentication.factory.AuthenticationFactory; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.common.constant.GrpcConstants; +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.ProxyContext; +import org.apache.rocketmq.proxy.common.utils.GrpcUtils; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; + +public class AuthenticationPipeline implements RequestPipeline { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + private final AuthConfig authConfig; + private final AuthenticationEvaluator authenticationEvaluator; + + public AuthenticationPipeline(AuthConfig authConfig, MessagingProcessor messagingProcessor) { + this.authConfig = authConfig; + this.authenticationEvaluator = AuthenticationFactory.getEvaluator(authConfig, messagingProcessor::getMetadataService); + } + + @Override + public void execute(ProxyContext context, Metadata headers, GeneratedMessageV3 request) { + if (!authConfig.isAuthenticationEnabled()) { + return; + } + try { + Metadata metadata = GrpcConstants.METADATA.get(Context.current()); + AuthenticationContext authenticationContext = newContext(context, metadata, request); + authenticationEvaluator.evaluate(authenticationContext); + } catch (AuthenticationException ex) { + throw ex; + } catch (Throwable ex) { + LOGGER.error("authenticate failed, request:{}", request, ex); + throw ex; + } + } + + /** + * Create Context, for extension + * + * @param context for extension + * @param headers gRPC headers + * @param request + * @return + */ + protected AuthenticationContext newContext(ProxyContext context, Metadata headers, GeneratedMessageV3 request) { + AuthenticationContext result = AuthenticationFactory.newContext(authConfig, headers, request); + if (result instanceof DefaultAuthenticationContext) { + DefaultAuthenticationContext defaultAuthenticationContext = (DefaultAuthenticationContext) result; + if (StringUtils.isNotBlank(defaultAuthenticationContext.getUsername())) { + GrpcUtils.putHeaderIfNotExist(headers, GrpcConstants.AUTHORIZATION_AK, defaultAuthenticationContext.getUsername()); + } + } + return result; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/pipeline/AuthorizationPipeline.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/pipeline/AuthorizationPipeline.java new file mode 100644 index 0000000..c0b3342 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/pipeline/AuthorizationPipeline.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.grpc.pipeline; + +import com.google.protobuf.GeneratedMessageV3; +import io.grpc.Metadata; +import java.util.List; +import org.apache.rocketmq.auth.authentication.exception.AuthenticationException; +import org.apache.rocketmq.auth.authorization.AuthorizationEvaluator; +import org.apache.rocketmq.auth.authorization.context.AuthorizationContext; +import org.apache.rocketmq.auth.authorization.exception.AuthorizationException; +import org.apache.rocketmq.auth.authorization.factory.AuthorizationFactory; +import org.apache.rocketmq.auth.config.AuthConfig; +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.ProxyContext; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; + +public class AuthorizationPipeline implements RequestPipeline { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + private final AuthConfig authConfig; + private final AuthorizationEvaluator authorizationEvaluator; + + public AuthorizationPipeline(AuthConfig authConfig, MessagingProcessor messagingProcessor) { + this.authConfig = authConfig; + this.authorizationEvaluator = AuthorizationFactory.getEvaluator(authConfig, messagingProcessor::getMetadataService); + } + + @Override + public void execute(ProxyContext context, Metadata headers, GeneratedMessageV3 request) { + if (!authConfig.isAuthorizationEnabled()) { + return; + } + try { + List contexts = newContexts(context, headers, request); + authorizationEvaluator.evaluate(contexts); + } catch (AuthorizationException | AuthenticationException ex) { + throw ex; + } catch (Throwable ex) { + LOGGER.error("authorize failed, request:{}", request, ex); + throw ex; + } + } + + protected List newContexts(ProxyContext context, Metadata headers, GeneratedMessageV3 request) { + return AuthorizationFactory.newContexts(authConfig, headers, request); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/pipeline/ContextInitPipeline.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/pipeline/ContextInitPipeline.java new file mode 100644 index 0000000..515b9ac --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/pipeline/ContextInitPipeline.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.grpc.pipeline; + +import com.google.protobuf.GeneratedMessageV3; +import io.grpc.Context; +import io.grpc.Metadata; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.constant.GrpcConstants; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.processor.channel.ChannelProtocolType; + +public class ContextInitPipeline implements RequestPipeline { + @Override + public void execute(ProxyContext context, Metadata headers, GeneratedMessageV3 request) { + Context ctx = Context.current(); + context.setLocalAddress(getDefaultStringMetadataInfo(headers, GrpcConstants.LOCAL_ADDRESS)) + .setRemoteAddress(getDefaultStringMetadataInfo(headers, GrpcConstants.REMOTE_ADDRESS)) + .setClientID(getDefaultStringMetadataInfo(headers, GrpcConstants.CLIENT_ID)) + .setProtocolType(ChannelProtocolType.GRPC_V2.getName()) + .setLanguage(getDefaultStringMetadataInfo(headers, GrpcConstants.LANGUAGE)) + .setClientVersion(getDefaultStringMetadataInfo(headers, GrpcConstants.CLIENT_VERSION)) + .setAction(getDefaultStringMetadataInfo(headers, GrpcConstants.SIMPLE_RPC_NAME)) + .setNamespace(getDefaultStringMetadataInfo(headers, GrpcConstants.NAMESPACE_ID)); + if (ctx.getDeadline() != null) { + context.setRemainingMs(ctx.getDeadline().timeRemaining(TimeUnit.MILLISECONDS)); + } + } + + protected String getDefaultStringMetadataInfo(Metadata headers, Metadata.Key key) { + return StringUtils.defaultString(headers.get(key)); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/pipeline/RequestPipeline.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/pipeline/RequestPipeline.java new file mode 100644 index 0000000..3423208 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/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.grpc.pipeline; + +import com.google.protobuf.GeneratedMessageV3; +import io.grpc.Metadata; +import org.apache.rocketmq.proxy.common.ProxyContext; + +public interface RequestPipeline { + + void execute(ProxyContext context, Metadata headers, GeneratedMessageV3 request); + + default RequestPipeline pipe(RequestPipeline source) { + return (ctx, headers, request) -> { + source.execute(ctx, headers, request); + execute(ctx, headers, request); + }; + } +} 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 0000000..6598b9e --- /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 0000000..c186bfb --- /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 0000000..3c6f120 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/DefaultGrpcMessingActivity.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.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.RecallMessageRequest; +import apache.rocketmq.v2.RecallMessageResponse; +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.RecallMessageActivity; +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 RecallMessageActivity recallMessageActivity; + 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.recallMessageActivity = new RecallMessageActivity(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 CompletableFuture recallMessage(ProxyContext ctx, + RecallMessageRequest request) { + return this.recallMessageActivity.recallMessage(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 0000000..c470eda --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/GrpcMessagingApplication.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.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.RecallMessageRequest; +import apache.rocketmq.v2.RecallMessageResponse; +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 com.google.protobuf.GeneratedMessageV3; +import io.grpc.Context; +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.auth.config.AuthConfig; +import org.apache.rocketmq.common.constant.GrpcConstants; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.thread.ThreadPoolMonitor; +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.ProxyConfig; +import org.apache.rocketmq.proxy.grpc.pipeline.AuthenticationPipeline; +import org.apache.rocketmq.proxy.grpc.pipeline.AuthorizationPipeline; +import org.apache.rocketmq.proxy.grpc.pipeline.ContextInitPipeline; +import org.apache.rocketmq.proxy.grpc.pipeline.RequestPipeline; +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; + +public class GrpcMessagingApplication extends MessagingServiceGrpc.MessagingServiceImplBase implements StartAndShutdown { + private final static Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + + private final GrpcMessingActivity grpcMessingActivity; + + protected final RequestPipeline requestPipeline; + + protected ThreadPoolExecutor routeThreadPoolExecutor; + protected ThreadPoolExecutor producerThreadPoolExecutor; + protected ThreadPoolExecutor consumerThreadPoolExecutor; + protected ThreadPoolExecutor clientManagerThreadPoolExecutor; + protected ThreadPoolExecutor transactionThreadPoolExecutor; + + + protected GrpcMessagingApplication(GrpcMessingActivity grpcMessingActivity, RequestPipeline requestPipeline) { + this.grpcMessingActivity = grpcMessingActivity; + this.requestPipeline = requestPipeline; + + 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) { + RequestPipeline pipeline = (context, headers, request) -> { + }; + // add pipeline + // the last pipe add will execute at the first + AuthConfig authConfig = ConfigurationManager.getAuthConfig(); + if (authConfig != null) { + pipeline = pipeline + .pipe(new AuthorizationPipeline(authConfig, messagingProcessor)) + .pipe(new AuthenticationPipeline(authConfig, messagingProcessor)); + } + pipeline = pipeline.pipe(new ContextInitPipeline()); + return new GrpcMessagingApplication(new DefaultGrpcMessingActivity(messagingProcessor), pipeline); + } + + 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) { + if (request instanceof GeneratedMessageV3) { + requestPipeline.execute(context, GrpcConstants.METADATA.get(Context.current()), (GeneratedMessageV3) request); + validateContext(context); + } else { + log.error("[BUG]grpc request pipe is not been executed"); + } + 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() { + return ProxyContext.create(); + } + + protected void validateContext(ProxyContext context) { + if (StringUtils.isBlank(context.getClientID())) { + throw new GrpcProxyException(Code.CLIENT_ID_REQUIRED, "client id cannot be empty"); + } + } + + @Override + public void queryRoute(QueryRouteRequest request, StreamObserver responseObserver) { + Function statusResponseCreator = status -> QueryRouteResponse.newBuilder().setStatus(status).build(); + ProxyContext context = createContext(); + try { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 void recallMessage(RecallMessageRequest request, StreamObserver responseObserver) { + Function statusResponseCreator = + status -> RecallMessageResponse.newBuilder().setStatus(status).build(); + ProxyContext context = createContext(); + try { + this.addExecutor(this.producerThreadPoolExecutor, // reuse producer thread pool + context, + request, + () -> grpcMessingActivity.recallMessage(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 { + 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 0000000..db15f25 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/GrpcMessingActivity.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.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.RecallMessageRequest; +import apache.rocketmq.v2.RecallMessageResponse; +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); + + CompletableFuture recallMessage(ProxyContext ctx, RecallMessageRequest 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 0000000..a18cf76 --- /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 0000000..f05251c --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/channel/GrpcClientChannel.java @@ -0,0 +1,271 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Objects; +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 (Objects.isNull(header) || !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 0000000..a46bc99 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/client/ClientActivity.java @@ -0,0 +1,492 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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); + if (clientSettings == null) { + future.complete(NotifyClientTerminationResponse.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(); + 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 0000000..e741bd3 --- /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 0000000..33a4e13 --- /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 0000000..74e499b --- /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 0000000..a556bfe --- /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 0000000..ee5fc01 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/common/ResponseBuilder.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.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.auth.authentication.exception.AuthenticationException; +import org.apache.rocketmq.auth.authorization.exception.AuthorizationException; +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.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()); + } + if (t instanceof AuthenticationException || t instanceof AuthorizationException) { + return buildStatus(Code.UNAUTHORIZED, 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 0000000..3ac3d48 --- /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 0000000..76019a1 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/AckMessageActivity.java @@ -0,0 +1,206 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.channel.GrpcClientChannel; +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(); + GrpcClientChannel channel = grpcChannelManager.getChannel(ctx.getClientID()); + if (channel != null) { + MessageReceiptHandle messageReceiptHandle = messagingProcessor.removeReceiptHandle(ctx, channel, 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 0000000..b7d63a3 --- /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 0000000..b311061 --- /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 0000000..50b6d92 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java @@ -0,0 +1,206 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.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())) { + GrpcClientChannel clientChannel = grpcChannelManager.getChannel(ctx.getClientID()); + if (clientChannel == null) { + GrpcProxyException e = new GrpcProxyException(Code.MESSAGE_NOT_FOUND, + String.format("The client [%s] is disconnected.", ctx.getClientID())); + popResult.getMsgFoundList().forEach(messageExt -> + writer.processThrowableWhenWriteMessage(e, ctx, request, messageExt)); + throw e; + } + 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, clientChannel, 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 0000000..bdeffbb --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageResponseStreamWriter.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.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 { + try { + streamObserver.onNext(ReceiveMessageResponse.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.OK, Code.OK.name())) + .build()); + } catch (Throwable t) { + messageFoundList.forEach(messageExt -> + this.processThrowableWhenWriteMessage(t, ctx, request, messageExt)); + throw t; + } + 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 0000000..d0cfc14 --- /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/RecallMessageActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/producer/RecallMessageActivity.java new file mode 100644 index 0000000..28ec97d --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/producer/RecallMessageActivity.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.grpc.v2.producer; + +import apache.rocketmq.v2.Code; +import apache.rocketmq.v2.RecallMessageRequest; +import apache.rocketmq.v2.RecallMessageResponse; +import apache.rocketmq.v2.Resource; +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 java.time.Duration; +import java.util.concurrent.CompletableFuture; + +public class RecallMessageActivity extends AbstractMessingActivity { + + public RecallMessageActivity(MessagingProcessor messagingProcessor, + GrpcClientSettingsManager grpcClientSettingsManager, GrpcChannelManager grpcChannelManager) { + super(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + } + + public CompletableFuture recallMessage(ProxyContext ctx, + RecallMessageRequest request) { + CompletableFuture future = new CompletableFuture<>(); + + try { + Resource topic = request.getTopic(); + validateTopic(topic); + + future = this.messagingProcessor.recallMessage( + ctx, + topic.getName(), + request.getRecallHandle(), + Duration.ofSeconds(2).toMillis() + ).thenApply(result -> RecallMessageResponse.newBuilder() + .setMessageId(result) + .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/grpc/v2/producer/SendMessageActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/producer/SendMessageActivity.java new file mode 100644 index 0000000..f7b8014 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/producer/SendMessageActivity.java @@ -0,0 +1,399 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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) { + if (ConfigurationManager.getProxyConfig().isEnableMessageBodyEmptyCheck()) { + if (body.isEmpty()) { + throw new GrpcProxyException(Code.MESSAGE_BODY_EMPTY, "message body cannot be empty"); + } + } + 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())) + .setRecallHandle(StringUtils.defaultString(result.getRecallHandle())) + .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 0000000..20ae3aa --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/route/RouteActivity.java @@ -0,0 +1,317 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; + int n = 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(); + } else if (!PermName.isAccessible(queueData.getPerm())) { + n = Math.max(1, Math.max(queueData.getWriteQueueNums(), 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); + } + + for (int i = 0; i < n; i++) { + MessageQueue messageQueue = MessageQueue.newBuilder().setBroker(broker).setTopic(topic) + .setId(queueIdIndex++) + .setPermission(Permission.NONE) + .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 0000000..ce143d5 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/transaction/EndTransactionActivity.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.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.getTopic().getName(), + 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 0000000..a7682e6 --- /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 0000000..2b8dac5 --- /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 0000000..c63212c --- /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 0000000..dfb9c9b --- /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 0000000..eeb9bf8 --- /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 0000000..ace8af1 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ConsumerProcessor.java @@ -0,0 +1,548 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.common.utils.FutureUtils; +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.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); + + 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 updateConsumerOffsetAsync(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().updateConsumerOffsetAsync(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 0000000..d0c0dd6 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/DefaultMessagingProcessor.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.proxy.processor; + +import com.alibaba.fastjson2.JSON; +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.commons.lang3.StringUtils; +import org.apache.rocketmq.acl.common.AclClientRPCHook; +import org.apache.rocketmq.acl.common.AclUtils; +import org.apache.rocketmq.acl.common.SessionCredentials; +import org.apache.rocketmq.auth.config.AuthConfig; +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()) { + return createForClusterMode(rpcHook); + } + AuthConfig authConfig = ConfigurationManager.getAuthConfig(); + if (StringUtils.isNotBlank(authConfig.getInnerClientAuthenticationCredentials())) { + SessionCredentials sessionCredentials = + JSON.parseObject(authConfig.getInnerClientAuthenticationCredentials(), SessionCredentials.class); + if (StringUtils.isNotBlank(sessionCredentials.getAccessKey()) && StringUtils.isNotBlank(sessionCredentials.getSecretKey())) { + rpcHook = new AclClientRPCHook(sessionCredentials); + } + } else { + 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 topic, String transactionId, + String messageId, String producerGroup, + TransactionStatus transactionStatus, boolean fromTransactionCheck, + long timeoutMillis) { + return this.transactionProcessor.endTransaction(ctx, topic, 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 updateConsumerOffsetAsync(ProxyContext ctx, MessageQueue messageQueue, + String consumerGroup, long commitOffset, long timeoutMillis) { + return this.consumerProcessor.updateConsumerOffsetAsync(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 recallMessage(ProxyContext ctx, String topic, + String recallHandle, long timeoutMillis) { + return this.producerProcessor.recallMessage(ctx, topic, recallHandle, 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 0000000..fee0465 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/MessagingProcessor.java @@ -0,0 +1,344 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.common.utils.StartAndShutdown; +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.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 topic, + String transactionId, + String messageId, + String producerGroup, + TransactionStatus transactionStatus, + boolean fromTransactionCheck + ) { + return endTransaction(ctx, topic, transactionId, messageId, producerGroup, transactionStatus, fromTransactionCheck, DEFAULT_TIMEOUT_MILLS); + } + + CompletableFuture endTransaction( + ProxyContext ctx, + String topic, + 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 updateConsumerOffsetAsync( + 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 recallMessage( + ProxyContext ctx, + String topic, + String recallHandle, + 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 0000000..09c1a0b --- /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 0000000..17a2f27 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ProducerProcessor.java @@ -0,0 +1,268 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.codec.DecoderException; +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.producer.RecallMessageHandle; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.common.utils.FutureUtils; +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.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.RecallMessageRequestHeader; +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 (isNeedCheckTopicMessageType(message)) { + 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); + } + + public CompletableFuture recallMessage(ProxyContext ctx, String topic, + String recallHandle, long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + try { + if (ConfigurationManager.getProxyConfig().isEnableTopicMessageTypeCheck()) { + TopicMessageType messageType = serviceManager.getMetadataService().getTopicMessageType(ctx, topic); + topicMessageTypeValidator.validate(messageType, TopicMessageType.DELAY); + } + + RecallMessageHandle.HandleV1 handleEntity; + try { + handleEntity = (RecallMessageHandle.HandleV1) RecallMessageHandle.decodeHandle(recallHandle); + } catch (DecoderException e) { + throw new ProxyException(ProxyExceptionCode.INTERNAL_SERVER_ERROR, e.getMessage()); + } + String brokerName = handleEntity.getBrokerName(); + RecallMessageRequestHeader requestHeader = new RecallMessageRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setRecallHandle(recallHandle); + requestHeader.setBrokerName(brokerName); + future = serviceManager.getMessageService().recallMessage(ctx, brokerName, requestHeader, timeoutMillis); + } 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(), + messageList.get(0).getTopic(), + 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); + } + + private boolean isNeedCheckTopicMessageType(Message message) { + return ConfigurationManager.getProxyConfig().isEnableTopicMessageTypeCheck() + && !message.hasProperty(MessageConst.PROPERTY_TRANSFER_FLAG); + } +} 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 0000000..5fe0d1c --- /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 0000000..5e1be93 --- /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 0000000..9f3187c --- /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 0000000..3450bb2 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/TransactionProcessor.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.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 topic, String transactionId, String messageId, String producerGroup, + TransactionStatus transactionStatus, boolean fromTransactionCheck, long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + try { + EndTransactionRequestData headerData = serviceManager.getTransactionService().genEndTransactionRequestHeader( + ctx, + topic, + 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 0000000..e456a60 --- /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 0000000..3538a94 --- /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 0000000..d2eeb83 --- /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 0000000..fb9666a --- /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 0000000..9f886e8 --- /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 0000000..a22401a --- /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 0000000..83588f1 --- /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 0000000..32758da --- /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 0000000..74eb6f2 --- /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 0000000..d7c2820 --- /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 0000000..5a21ec6 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/MultiProtocolTlsHelper.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.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; + } + + String authModeUpper = authMode.toUpperCase(); + for (ClientAuth clientAuth : ClientAuth.values()) { + if (clientAuth.name().equals(authModeUpper)) { + 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 0000000..3fae056 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/RemotingProtocolServer.java @@ -0,0 +1,385 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.rocketmq.auth.config.AuthConfig; +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.RecallMessageActivity; +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.AuthorizationPipeline; +import org.apache.rocketmq.proxy.remoting.pipeline.ContextInitPipeline; +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; + +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; + +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 RecallMessageActivity recallMessageActivity; + 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) { + this.messagingProcessor = messagingProcessor; + this.remotingChannelManager = new RemotingChannelManager(this, messagingProcessor.getProxyRelayService()); + + RequestPipeline pipeline = createRequestPipeline(messagingProcessor); + 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.recallMessageActivity = new RecallMessageActivity(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.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); + + this.registerRemotingServer(this.defaultRemotingServer); + } + + 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.RECALL_MESSAGE, recallMessageActivity, 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(MessagingProcessor messagingProcessor) { + RequestPipeline pipeline = (ctx, request, context) -> { + }; + // add pipeline + // the last pipe add will execute at the first + AuthConfig authConfig = ConfigurationManager.getAuthConfig(); + if (authConfig != null) { + pipeline = pipeline.pipe(new AuthorizationPipeline(authConfig, messagingProcessor)) + .pipe(new AuthenticationPipeline(authConfig, messagingProcessor)); + } + return pipeline.pipe(new ContextInitPipeline()); + } + + 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 0000000..5a96c41 --- /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 0000000..f3d8fb6 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/AbstractRemotingActivity.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.proxy.remoting.activity; + +import io.netty.channel.ChannelHandlerContext; +import java.util.HashMap; +import java.util.Map; +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.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.utils.ExceptionUtils; +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.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.remoting.netty.NettyRequestProcessor; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; + +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 || request.getCode() == RequestCode.SEND_BATCH_MESSAGE) { + 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(); + 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() { + return ProxyContext.create(); + } + + 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 0000000..723b591 --- /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 0000000..9f6de99 --- /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 0000000..05d8e5f --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/ClientManagerActivity.java @@ -0,0 +1,204 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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()); + if (channel != null) { + ClientChannelInfo clientChannelInfo = new ClientChannelInfo( + channel, + requestHeader.getClientID(), + request.getLanguage(), + request.getVersion()); + this.messagingProcessor.unRegisterProducer(context, producerGroup, clientChannelInfo); + } else { + log.warn("unregister producer failed, channel not exist, may has been removed, producerGroup={}, channel={}", producerGroup, ctx.channel()); + } + } + final String consumerGroup = requestHeader.getConsumerGroup(); + if (consumerGroup != null) { + RemotingChannel channel = this.remotingChannelManager.removeConsumerChannel(context, consumerGroup, ctx.channel()); + if (channel != null) { + ClientChannelInfo clientChannelInfo = new ClientChannelInfo( + channel, + requestHeader.getClientID(), + request.getLanguage(), + request.getVersion()); + this.messagingProcessor.unRegisterConsumer(context, consumerGroup, clientChannelInfo); + } else { + log.warn("unregister consumer failed, channel not exist, may has been removed, consumerGroup={}, channel={}", consumerGroup, ctx.channel()); + } + } + 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 0000000..b21b4af --- /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 0000000..56ec34f --- /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(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 0000000..a635e55 --- /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 0000000..3324c23 --- /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/RecallMessageActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/RecallMessageActivity.java new file mode 100644 index 0000000..6f17745 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/RecallMessageActivity.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.remoting.activity; + +import io.netty.channel.ChannelHandlerContext; +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.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; +import org.apache.rocketmq.remoting.protocol.header.RecallMessageRequestHeader; + +import java.time.Duration; + +public class RecallMessageActivity extends AbstractRemotingActivity { + TopicMessageTypeValidator topicMessageTypeValidator; + + public RecallMessageActivity(RequestPipeline requestPipeline, + MessagingProcessor messagingProcessor) { + super(requestPipeline, messagingProcessor); + this.topicMessageTypeValidator = new DefaultTopicMessageTypeValidator(); + } + + @Override + public RemotingCommand processRequest0(ChannelHandlerContext ctx, RemotingCommand request, + ProxyContext context) throws Exception { + RecallMessageRequestHeader requestHeader = request.decodeCommandCustomHeader(RecallMessageRequestHeader.class); + String topic = requestHeader.getTopic(); + if (ConfigurationManager.getProxyConfig().isEnableTopicMessageTypeCheck()) { + TopicMessageType messageType = messagingProcessor.getMetadataService().getTopicMessageType(context, topic); + topicMessageTypeValidator.validate(messageType, TopicMessageType.DELAY); + } + return request(ctx, request, context, Duration.ofSeconds(2).toMillis()); + } +} 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 0000000..22d9efd --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/SendMessageActivity.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.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.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +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.NamespaceUtil; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; + +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 (isNeedCheckTopicMessageType(property)) { + 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()); + } + + private boolean isNeedCheckTopicMessageType(Map property) { + return ConfigurationManager.getProxyConfig().isEnableTopicMessageTypeCheck() + && !property.containsKey(MessageConst.PROPERTY_TRANSFER_FLAG); + } +} 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 0000000..b6cd9d0 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/TransactionActivity.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.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.getTopic(), + 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 0000000..5dbdea1 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/channel/RemotingChannel.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.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.common.utils.ExceptionUtils; +import org.apache.rocketmq.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.setTopic(messageExt.getTopic()); + 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 0000000..211c3c9 --- /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 0000000..2bd53d8 --- /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 0000000..1bfc484 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/pipeline/AuthenticationPipeline.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.proxy.remoting.pipeline; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.auth.authentication.AuthenticationEvaluator; +import org.apache.rocketmq.auth.authentication.context.AuthenticationContext; +import org.apache.rocketmq.auth.authentication.exception.AuthenticationException; +import org.apache.rocketmq.auth.authentication.factory.AuthenticationFactory; +import org.apache.rocketmq.auth.config.AuthConfig; +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.ProxyContext; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public class AuthenticationPipeline implements RequestPipeline { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + private final AuthConfig authConfig; + private final AuthenticationEvaluator authenticationEvaluator; + + public AuthenticationPipeline(AuthConfig authConfig, MessagingProcessor messagingProcessor) { + this.authConfig = authConfig; + this.authenticationEvaluator = AuthenticationFactory.getEvaluator(authConfig, messagingProcessor::getMetadataService); + } + + @Override + public void execute(ChannelHandlerContext ctx, RemotingCommand request, ProxyContext context) throws Exception { + if (!authConfig.isAuthenticationEnabled()) { + return; + } + try { + AuthenticationContext authenticationContext = newContext(ctx, request, context); + authenticationEvaluator.evaluate(authenticationContext); + } catch (AuthenticationException ex) { + throw ex; + } catch (Throwable ex) { + LOGGER.error("authenticate failed, request:{}", request, ex); + throw ex; + } + } + + protected AuthenticationContext newContext(ChannelHandlerContext ctx, RemotingCommand request, ProxyContext context) { + return AuthenticationFactory.newContext(authConfig, ctx, request); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/pipeline/AuthorizationPipeline.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/pipeline/AuthorizationPipeline.java new file mode 100644 index 0000000..49eb647 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/pipeline/AuthorizationPipeline.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.remoting.pipeline; + +import io.netty.channel.ChannelHandlerContext; +import java.util.List; +import org.apache.rocketmq.auth.authentication.exception.AuthenticationException; +import org.apache.rocketmq.auth.authorization.AuthorizationEvaluator; +import org.apache.rocketmq.auth.authorization.context.AuthorizationContext; +import org.apache.rocketmq.auth.authorization.exception.AuthorizationException; +import org.apache.rocketmq.auth.authorization.factory.AuthorizationFactory; +import org.apache.rocketmq.auth.config.AuthConfig; +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.ProxyContext; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public class AuthorizationPipeline implements RequestPipeline { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + private final AuthConfig authConfig; + private final AuthorizationEvaluator authorizationEvaluator; + + public AuthorizationPipeline(AuthConfig authConfig, MessagingProcessor messagingProcessor) { + this.authConfig = authConfig; + this.authorizationEvaluator = AuthorizationFactory.getEvaluator(authConfig, messagingProcessor::getMetadataService); + } + + @Override + public void execute(ChannelHandlerContext ctx, RemotingCommand request, ProxyContext context) throws Exception { + if (!authConfig.isAuthorizationEnabled()) { + return; + } + try { + List contexts = newContexts(request, ctx, context); + authorizationEvaluator.evaluate(contexts); + } catch (AuthorizationException | AuthenticationException ex) { + throw ex; + } catch (Throwable ex) { + LOGGER.error("authorize failed, request:{}", request, ex); + throw ex; + } + } + + protected List newContexts(RemotingCommand request, ChannelHandlerContext ctx, ProxyContext context) { + return AuthorizationFactory.newContexts(authConfig, ctx, request); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/pipeline/ContextInitPipeline.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/pipeline/ContextInitPipeline.java new file mode 100644 index 0000000..0a30f62 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/pipeline/ContextInitPipeline.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.remoting.pipeline; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.processor.channel.ChannelProtocolType; +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; + +public class ContextInitPipeline implements RequestPipeline { + + @Override + public void execute(ChannelHandlerContext ctx, RemotingCommand request, ProxyContext context) throws Exception { + Channel channel = ctx.channel(); + LanguageCode languageCode = RemotingHelper.getAttributeValue(AttributeKeys.LANGUAGE_CODE_KEY, channel); + String clientId = RemotingHelper.getAttributeValue(AttributeKeys.CLIENT_ID_KEY, channel); + Integer version = RemotingHelper.getAttributeValue(AttributeKeys.VERSION_KEY, channel); + context.setAction(RemotingHelper.getRequestCodeDesc(request.getCode())) + .setProtocolType(ChannelProtocolType.REMOTING.getName()) + .setChannel(channel) + .setLocalAddress(NetworkUtil.socketAddress2String(ctx.channel().localAddress())) + .setRemoteAddress(RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + if (languageCode != null) { + context.setLanguage(languageCode.name()); + } + if (clientId != null) { + context.setClientID(clientId); + } + if (version != null) { + context.setClientVersion(MQVersion.getVersionDesc(version)); + } + } +} 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 0000000..4c46a6e --- /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 0000000..4b1b030 --- /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 0000000..da2dded --- /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 0000000..5188688 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/protocol/http2proxy/HAProxyMessageForwarder.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.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 java.lang.reflect.Field; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +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.CommonConstants; +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.common.RemotingHelper; +import org.apache.rocketmq.remoting.netty.AttributeKeys; + +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 instanceof DefaultAttributeMap)) { + return; + } + + HAProxyMessage message = buildHAProxyMessage(inboundChannel); + if (message == null) { + return; + } + + outboundChannel.writeAndFlush(message).sync(); + } + + protected HAProxyMessage buildHAProxyMessage(Channel inboundChannel) throws IllegalAccessException, DecoderException { + String sourceAddress = null, destinationAddress = null; + int sourcePort = 0, destinationPort = 0; + if (inboundChannel.hasAttr(AttributeKeys.PROXY_PROTOCOL_ADDR)) { + Attribute[] attributes = (Attribute[]) FieldUtils.readField(FIELD_ATTRIBUTE, inboundChannel); + if (ArrayUtils.isEmpty(attributes)) { + return null; + } + 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); + } + } + } else { + String remoteAddr = RemotingHelper.parseChannelRemoteAddr(inboundChannel); + sourceAddress = StringUtils.substringBeforeLast(remoteAddr, CommonConstants.COLON); + sourcePort = Integer.parseInt(StringUtils.substringAfterLast(remoteAddr, CommonConstants.COLON)); + + String localAddr = RemotingHelper.parseChannelLocalAddr(inboundChannel); + destinationAddress = StringUtils.substringBeforeLast(localAddr, CommonConstants.COLON); + destinationPort = Integer.parseInt(StringUtils.substringAfterLast(localAddr, CommonConstants.COLON)); + } + + HAProxyProxiedProtocol proxiedProtocol = AclUtils.isColon(sourceAddress) ? HAProxyProxiedProtocol.TCP6 : + HAProxyProxiedProtocol.TCP4; + + List haProxyTLVs = buildHAProxyTLV(inboundChannel); + + return new HAProxyMessage(HAProxyProtocolVersion.V2, HAProxyCommand.PROXY, + proxiedProtocol, sourceAddress, destinationAddress, sourcePort, destinationPort, haProxyTLVs); + } + + protected List buildHAProxyTLV(Channel inboundChannel) throws IllegalAccessException, DecoderException { + List result = new ArrayList<>(); + if (!inboundChannel.hasAttr(AttributeKeys.PROXY_PROTOCOL_ADDR)) { + return result; + } + Attribute[] attributes = (Attribute[]) FieldUtils.readField(FIELD_ATTRIBUTE, inboundChannel); + if (ArrayUtils.isEmpty(attributes)) { + return result; + } + for (Attribute attribute : attributes) { + String attributeKey = attribute.key().name(); + if (!StringUtils.startsWith(attributeKey, HAProxyConstants.PROXY_PROTOCOL_TLV_PREFIX)) { + continue; + } + String attributeValue = (String) attribute.get(); + HAProxyTLV haProxyTLV = buildHAProxyTLV(attributeKey, attributeValue); + if (haProxyTLV != null) { + result.add(haProxyTLV); + } + } + return result; + } + + 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 0000000..103b099 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/protocol/http2proxy/Http2ProtocolProxyHandler.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.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 javax.net.ssl.SSLException; +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.TlsSystemConfig; + +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) { + 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 0000000..fd5408f --- /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 0000000..9b37e85 --- /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 0000000..49fea89 --- /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 0000000..33b65d2 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/ClusterServiceManager.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.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.ObjectCreator; +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; +import org.apache.rocketmq.remoting.RemotingClient; + +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) { + this(rpcHook, null); + } + + public ClusterServiceManager(RPCHook rpcHook, ObjectCreator remotingClientCreator) { + 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, + remotingClientCreator + ); + + this.operationClientAPIFactory = new MQClientAPIFactory( + nameserverAccessConfig, + "OperationClient_", + 1, + new DoNothingClientRemotingProcessor(null), + rpcHook, + this.scheduledExecutorService, + remotingClientCreator + ); + + 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, + remotingClientCreator + ); + + 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 0000000..59cd926 --- /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 0000000..c271eca --- /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 0000000..e1252fe --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/ServiceManagerFactory.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.service; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.ObjectCreator; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.RemotingClient; + +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, null); + } + + public static ServiceManager createForClusterMode(RPCHook rpcHook) { + return createForClusterMode(rpcHook, null); + } + + public static ServiceManager createForClusterMode(RPCHook rpcHook, ObjectCreator remotingClientCreator) { + return new ClusterServiceManager(rpcHook, remotingClientCreator); + } +} 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 0000000..a9e6686 --- /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 0000000..f3c68ea --- /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 0000000..323c8c5 --- /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 0000000..00e8cea --- /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 0000000..9fb488e --- /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 0000000..0db9516 --- /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 0000000..65c1fd4 --- /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 0000000..801c62e --- /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 0000000..65a4569 --- /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 0000000..655ce7e --- /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 0000000..f6f3406 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/message/ClusterMessageService.java @@ -0,0 +1,283 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.common.utils.FutureUtils; +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.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.RecallMessageRequestHeader; +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 updateConsumerOffsetAsync(ProxyContext ctx, AddressableMessageQueue messageQueue, + UpdateConsumerOffsetRequestHeader requestHeader, long timeoutMillis) { + return this.mqClientAPIFactory.getClient().updateConsumerOffsetAsync( + 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 recallMessage(ProxyContext ctx, String brokerName, + RecallMessageRequestHeader requestHeader, long timeoutMillis) { + return this.mqClientAPIFactory.getClient().recallMessageAsync( + this.resolveBrokerAddr(ctx, brokerName), + 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 0000000..cb9b7a4 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/message/LocalMessageService.java @@ -0,0 +1,513 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.RecallMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.RecallMessageResponseHeader; +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()); + sendResult.setRecallHandle(responseHeader.getRecallHandle()); + 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) { + requestHeader.setBornTime(System.currentTimeMillis()); + 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 { + future = brokerController.getChangeInvisibleTimeProcessor() + .processRequestAsync(channelHandlerContext.channel(), command, true); + } 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 updateConsumerOffsetAsync(ProxyContext ctx, AddressableMessageQueue messageQueue, + UpdateConsumerOffsetRequestHeader requestHeader, long timeoutMillis) { + throw new NotImplementedException("updateConsumerOffsetAsync 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 recallMessage(ProxyContext ctx, String brokerName, + RecallMessageRequestHeader requestHeader, long timeoutMillis) { + SimpleChannel channel = channelManager.createChannel(ctx); + ChannelHandlerContext channelHandlerContext = channel.getChannelHandlerContext(); + RemotingCommand command = + LocalRemotingCommand.createRequestCommand(RequestCode.RECALL_MESSAGE, requestHeader, ctx.getLanguage()); + CompletableFuture future = new CompletableFuture<>(); + try { + RemotingCommand response = brokerController.getRecallMessageProcessor() + .processRequest(channelHandlerContext, command); + future.complete(response); + } catch (Exception e) { + log.error("Fail to process recallMessage command", e); + future.completeExceptionally(e); + } + return future.thenApply(r -> { + switch (r.getCode()) { + case ResponseCode.SUCCESS: + return ((RecallMessageResponseHeader) r.readCustomHeader()).getMsgId(); + default: + throw new ProxyException(ProxyExceptionCode.INTERNAL_SERVER_ERROR, r.getRemark()); + } + }); + } + + @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 0000000..25066ea --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/message/LocalRemotingCommand.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.service.message; + +import java.util.HashMap; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +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; + } +} 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 0000000..80f5ae7 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/message/MessageService.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.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.RecallMessageRequestHeader; +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 updateConsumerOffsetAsync( + 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 recallMessage( + ProxyContext ctx, + String brokerName, + RecallMessageRequestHeader 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 0000000..ae63fed --- /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 0000000..70ce1d3 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/metadata/ClusterMetadataService.java @@ -0,0 +1,291 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.List; +import java.util.Optional; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.auth.authentication.model.Subject; +import org.apache.rocketmq.auth.authentication.model.User; +import org.apache.rocketmq.auth.authorization.model.Acl; +import org.apache.rocketmq.broker.auth.converter.AclConverter; +import org.apache.rocketmq.broker.auth.converter.UserConverter; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIFactory; +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.common.utils.AbstractStartAndShutdown; +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.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.proxy.service.route.TopicRouteHelper; +import org.apache.rocketmq.proxy.service.route.TopicRouteService; +import org.apache.rocketmq.remoting.protocol.body.AclInfo; +import org.apache.rocketmq.remoting.protocol.body.UserInfo; +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(); + + protected final LoadingCache userCache; + + protected final static User EMPTY_USER = new User(); + + protected final LoadingCache aclCache; + + protected final static Acl EMPTY_ACL = new Acl(); + + protected final Random random = new Random(); + + + 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.userCache = CacheBuilder.newBuilder() + .maximumSize(config.getUserCacheMaxNum()) + .expireAfterAccess(config.getUserCacheExpiredSeconds(), TimeUnit.SECONDS) + .refreshAfterWrite(config.getUserCacheRefreshSeconds(), TimeUnit.SECONDS) + .build(new ClusterUserCacheLoader()); + this.aclCache = CacheBuilder.newBuilder() + .maximumSize(config.getAclCacheMaxNum()) + .expireAfterAccess(config.getAclCacheExpiredSeconds(), TimeUnit.SECONDS) + .refreshAfterWrite(config.getAclCacheRefreshSeconds(), TimeUnit.SECONDS) + .build(new ClusterAclCacheLoader()); + + 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; + } + + @Override + public CompletableFuture getUser(ProxyContext ctx, String username) { + CompletableFuture result = new CompletableFuture<>(); + try { + User user = this.userCache.get(username); + if (user == EMPTY_USER) { + user = null; + } + result.complete(user); + } catch (Exception e) { + result.completeExceptionally(e); + } + return result; + } + + @Override + public CompletableFuture getAcl(ProxyContext ctx, Subject subject) { + CompletableFuture result = new CompletableFuture<>(); + try { + Acl acl = this.aclCache.get(subject.getSubjectKey()); + if (acl == EMPTY_ACL) { + acl = null; + } + result.complete(acl); + } catch (Exception e) { + result.completeExceptionally(e); + } + return result; + } + + 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 class ClusterUserCacheLoader extends AbstractCacheLoader { + + public ClusterUserCacheLoader() { + super(cacheRefreshExecutor); + } + + @Override + protected User getDirectly(String username) throws Exception { + ProxyConfig config = ConfigurationManager.getProxyConfig(); + String clusterName = config.getRocketMQClusterName(); + Optional brokerDataOptional = findOneBroker(clusterName); + if (brokerDataOptional.isPresent()) { + String brokerAddress = brokerDataOptional.get().selectBrokerAddr(); + UserInfo userInfo = mqClientAPIFactory.getClient().getUser(brokerAddress, username, DEFAULT_TIMEOUT); + if (userInfo == null) { + return EMPTY_USER; + } + return UserConverter.convertUser(userInfo); + } + return EMPTY_USER; + } + + @Override + protected void onErr(String key, Exception e) { + log.error("load user failed. username:{}", key, e); + } + } + + protected class ClusterAclCacheLoader extends AbstractCacheLoader { + + public ClusterAclCacheLoader() { + super(cacheRefreshExecutor); + } + + @Override + protected Acl getDirectly(String subject) throws Exception { + ProxyConfig config = ConfigurationManager.getProxyConfig(); + String clusterName = config.getRocketMQClusterName(); + Optional brokerDataOptional = findOneBroker(clusterName); + if (brokerDataOptional.isPresent()) { + String brokerAddress = brokerDataOptional.get().selectBrokerAddr(); + AclInfo aclInfo = mqClientAPIFactory.getClient().getAcl(brokerAddress, subject, DEFAULT_TIMEOUT); + if (aclInfo == null) { + return EMPTY_ACL; + } + return AclConverter.convertAcl(aclInfo); + } + return EMPTY_ACL; + } + + @Override + protected void onErr(String key, Exception e) { + log.error("load user failed. username:{}", key, e); + } + } + + protected Optional findOneBroker(String topic) throws Exception { + try { + List brokerDatas = topicRouteService.getAllMessageQueueView(ProxyContext.createForInner(this.getClass()), topic).getTopicRouteData().getBrokerDatas(); + int skipNum = random.nextInt(brokerDatas.size()); + return brokerDatas.stream().skip(skipNum).findFirst(); + } 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 0000000..7b43f76 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/metadata/LocalMetadataService.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.service.metadata; + +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.auth.authentication.model.Subject; +import org.apache.rocketmq.auth.authentication.model.User; +import org.apache.rocketmq.auth.authorization.model.Acl; +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); + } + + @Override + public CompletableFuture getUser(ProxyContext ctx, String username) { + return this.brokerController.getAuthenticationMetadataManager().getUser(username); + } + + @Override + public CompletableFuture getAcl(ProxyContext ctx, Subject subject) { + return this.brokerController.getAuthorizationMetadataManager().getAcl(subject); + } +} 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 0000000..bc8654d --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/metadata/MetadataService.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.proxy.service.metadata; + +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.auth.authentication.model.Subject; +import org.apache.rocketmq.auth.authentication.model.User; +import org.apache.rocketmq.auth.authorization.model.Acl; +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); + + CompletableFuture getUser(ProxyContext ctx, String username); + + CompletableFuture getAcl(ProxyContext ctx, Subject subject); +} 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 0000000..0cb5193 --- /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.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(createContext("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(ProxyContext context, ReceiptHandleGroupKey key, ReceiptHandleGroup group, String msgID, String handleStr) { + try { + group.computeIfPresent(msgID, handleStr, messageReceiptHandle -> startRenewMessage(context, key, messageReceiptHandle)); + } catch (Exception e) { + log.error("error when renew message. msgID:{}, handleStr:{}", msgID, handleStr, e); + } + } + + protected CompletableFuture startRenewMessage(ProxyContext context, 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 { + 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 0000000..6a8888e --- /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 0000000..d881a51 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/relay/AbstractProxyRelayService.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.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), + messageExt.getTopic(), + 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 0000000..71ce222 --- /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 0000000..9fcc27f --- /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 0000000..5a1185a --- /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 0000000..95b98d4 --- /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 0000000..96d3b69 --- /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 0000000..20ee0f5 --- /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 0000000..ca877f3 --- /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 0000000..a4df989 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/ClusterTopicRouteService.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.service.route; + +import java.util.List; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIFactory; +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.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(); + return new ProxyTopicRouteData(topicRouteData, requestHostAndPortList); + } + + @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 0000000..f2a42c0 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/LocalTopicRouteService.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.proxy.service.route; + +import com.google.common.collect.Lists; +import java.util.HashMap; +import java.util.List; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIFactory; +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.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(); + return new ProxyTopicRouteData(topicRouteData, grpcPort); + } + + @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 0000000..f25fb90 --- /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 0000000..898e529 --- /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 0000000..4c33580 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/ProxyTopicRouteData.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.route; + +import com.google.common.collect.Lists; +import com.google.common.net.HostAndPort; +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 ProxyTopicRouteData() { + } + + public ProxyTopicRouteData(TopicRouteData topicRouteData) { + this.queueDatas = topicRouteData.getQueueDatas(); + this.brokerDatas = new ArrayList<>(); + + for (BrokerData brokerData : topicRouteData.getBrokerDatas()) { + ProxyTopicRouteData.ProxyBrokerData proxyBrokerData = new ProxyTopicRouteData.ProxyBrokerData(); + proxyBrokerData.setCluster(brokerData.getCluster()); + proxyBrokerData.setBrokerName(brokerData.getBrokerName()); + brokerData.getBrokerAddrs().forEach((brokerId, brokerAddr) -> { + HostAndPort brokerHostAndPort = HostAndPort.fromString(brokerAddr); + + proxyBrokerData.getBrokerAddrs().put(brokerId, Lists.newArrayList(new Address(brokerHostAndPort))); + }); + this.brokerDatas.add(proxyBrokerData); + } + } + + public ProxyTopicRouteData(TopicRouteData topicRouteData, int port) { + this.queueDatas = topicRouteData.getQueueDatas(); + this.brokerDatas = new ArrayList<>(); + + for (BrokerData brokerData : topicRouteData.getBrokerDatas()) { + ProxyTopicRouteData.ProxyBrokerData proxyBrokerData = new ProxyTopicRouteData.ProxyBrokerData(); + proxyBrokerData.setCluster(brokerData.getCluster()); + proxyBrokerData.setBrokerName(brokerData.getBrokerName()); + brokerData.getBrokerAddrs().forEach((brokerId, brokerAddr) -> { + HostAndPort brokerHostAndPort = HostAndPort.fromString(brokerAddr); + HostAndPort proxyHostAndPort = HostAndPort.fromParts(brokerHostAndPort.getHost(), port); + + proxyBrokerData.getBrokerAddrs().put(brokerId, Lists.newArrayList(new Address(proxyHostAndPort))); + }); + this.brokerDatas.add(proxyBrokerData); + } + } + + public ProxyTopicRouteData(TopicRouteData topicRouteData, List
    requestHostAndPortList) { + this.queueDatas = topicRouteData.getQueueDatas(); + this.brokerDatas = new ArrayList<>(); + + 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); + } + this.brokerDatas.add(proxyBrokerData); + } + } + + 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 0000000..651010c --- /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 0000000..bcdf814 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/TopicRouteService.java @@ -0,0 +1,229 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 java.time.Duration; +import java.util.List; +import java.util.Optional; +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.empty(); + } + 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 0000000..7956c62 --- /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 0000000..6c19edf --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/AbstractSystemMessageSyncer.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.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() { + 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 0000000..fee3ea8 --- /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 0000000..9776050 --- /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 0000000..8f0801f --- /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 0000000..5c9820d --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/transaction/AbstractTransactionService.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.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 topic, String producerGroup, long tranStateTableOffset, long commitLogOffset, String transactionId, + Message message) { + return this.addTransactionDataByBrokerName(ctx, this.getBrokerNameByAddr(brokerAddr), topic, producerGroup, tranStateTableOffset, commitLogOffset, transactionId, message); + } + + @Override + public TransactionData addTransactionDataByBrokerName(ProxyContext ctx, String brokerName, String topic, String producerGroup, long tranStateTableOffset, long commitLogOffset, String transactionId, + Message message) { + if (StringUtils.isBlank(brokerName)) { + return null; + } + TransactionData transactionData = new TransactionData( + brokerName, + topic, + tranStateTableOffset, commitLogOffset, transactionId, + System.currentTimeMillis(), + ConfigurationManager.getProxyConfig().getTransactionDataExpireMillis()); + + this.transactionDataManager.addTransactionData( + producerGroup, + transactionId, + transactionData + ); + return transactionData; + } + + @Override + public EndTransactionRequestData genEndTransactionRequestHeader(ProxyContext ctx, String topic, 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.setTopic(topic); + 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 0000000..1ec4286 --- /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 0000000..dbf2476 --- /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 0000000..4a27e4f --- /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 0000000..b4bfe40 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/transaction/TransactionData.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.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 String topic; + private final long tranStateTableOffset; + private final long commitLogOffset; + private final String transactionId; + private final long checkTimestamp; + private final long expireMs; + + public TransactionData(String brokerName, String topic, long tranStateTableOffset, long commitLogOffset, String transactionId, + long checkTimestamp, long expireMs) { + this.brokerName = brokerName; + this.topic = topic; + this.tranStateTableOffset = tranStateTableOffset; + this.commitLogOffset = commitLogOffset; + this.transactionId = transactionId; + this.checkTimestamp = checkTimestamp; + this.expireMs = expireMs; + } + + public String getBrokerName() { + return brokerName; + } + + public String getTopic() { + return topic; + } + + 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 0000000..81cd26e --- /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 0000000..89ebca4 --- /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 topic, String producerGroup, long tranStateTableOffset, long commitLogOffset, String transactionId, + Message message); + + TransactionData addTransactionDataByBrokerName(ProxyContext ctx, String brokerName, String topic, String producerGroup, long tranStateTableOffset, long commitLogOffset, String transactionId, + Message message); + + EndTransactionRequestData genEndTransactionRequestHeader(ProxyContext ctx, String topic, 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 0000000..aee4cbc --- /dev/null +++ b/proxy/src/main/resources/rmq.proxy.logback.xml @@ -0,0 +1,491 @@ + + + + + + + ${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 + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}auth_audit.log + + true + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}auth_audit.%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}proxy_traffic.log + + true + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}proxy_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/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 0000000..58213df --- /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/AddressTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/common/AddressTest.java new file mode 100644 index 0000000..b0df5ba --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/common/AddressTest.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.common; + +import com.google.common.net.HostAndPort; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +public class AddressTest { + + @Test + public void testConstructorWithIPv4() { + HostAndPort hostAndPort = HostAndPort.fromString("192.168.1.1:8080"); + Address address = new Address(hostAndPort); + + assertEquals(Address.AddressScheme.IPv4, address.getAddressScheme()); + assertEquals(hostAndPort, address.getHostAndPort()); + } + + @Test + public void testConstructorWithIPv6() { + HostAndPort hostAndPort = HostAndPort.fromString("[2001:db8::1]:8080"); + Address address = new Address(hostAndPort); + + assertEquals(Address.AddressScheme.IPv6, address.getAddressScheme()); + assertEquals(hostAndPort, address.getHostAndPort()); + } + + @Test + public void testConstructorWithDomainName() { + HostAndPort hostAndPort = HostAndPort.fromString("example.com:8080"); + Address address = new Address(hostAndPort); + + assertEquals(Address.AddressScheme.DOMAIN_NAME, address.getAddressScheme()); + assertEquals(hostAndPort, address.getHostAndPort()); + } + + @Test + public void testConstructorWithNullHostAndPort() { + Address address = new Address(null); + + assertEquals(Address.AddressScheme.UNRECOGNIZED, address.getAddressScheme()); + assertNull(address.getHostAndPort()); + } +} \ No newline at end of file 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 0000000..fdc1c5a --- /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.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 0000000..54e6272 --- /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 0000000..7c9d840 --- /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 0000000..7480360 --- /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 0000000..d71d163 --- /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 0000000..60c754e --- /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 0000000..699491f --- /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 0000000..3c29673 --- /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 0000000..5ae16eb --- /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.common.constant.GrpcConstants; +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(GrpcConstants.CLIENT_ID, CLIENT_ID); + metadata.put(GrpcConstants.LANGUAGE, JAVA); + metadata.put(GrpcConstants.REMOTE_ADDRESS, REMOTE_ADDR); + metadata.put(GrpcConstants.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 0000000..74d59a2 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/GrpcMessagingApplicationTest.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.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.common.constant.GrpcConstants; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.InitConfigTest; +import org.apache.rocketmq.proxy.grpc.pipeline.ContextInitPipeline; +import org.apache.rocketmq.proxy.grpc.pipeline.RequestPipeline; +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(); + RequestPipeline pipeline = (context, headers, request) -> { + }; + pipeline = pipeline.pipe(new ContextInitPipeline()); + grpcMessagingApplication = new GrpcMessagingApplication(grpcMessingActivity, pipeline); + } + + @Test + public void testQueryRoute() { + Metadata metadata = new Metadata(); + metadata.put(GrpcConstants.CLIENT_ID, CLIENT_ID); + metadata.put(GrpcConstants.LANGUAGE, JAVA); + metadata.put(GrpcConstants.REMOTE_ADDRESS, REMOTE_ADDR); + metadata.put(GrpcConstants.LOCAL_ADDRESS, LOCAL_ADDR); + + Assert.assertNotNull(Context.current() + .withValue(GrpcConstants.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(GrpcConstants.LANGUAGE, JAVA); + metadata.put(GrpcConstants.REMOTE_ADDRESS, REMOTE_ADDR); + metadata.put(GrpcConstants.LOCAL_ADDRESS, LOCAL_ADDR); + + Assert.assertNotNull(Context.current() + .withValue(GrpcConstants.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 0000000..1bdbdd9 --- /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 0000000..0c1ebcd --- /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 0000000..6742f09 --- /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 0000000..bc9b8a6 --- /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 0000000..df42844 --- /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 0000000..3c47461 --- /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 0000000..2de9a06 --- /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 0000000..b002db1 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivityTest.java @@ -0,0 +1,406 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +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.PopStatus; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +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.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; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +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 testReceiveMessageAddReceiptHandle() { + ConfigurationManager.getProxyConfig().setEnableProxyAutoRenew(true); + StreamObserver receiveStreamObserver = mock(ServerCallStreamObserver.class); + doNothing().when(receiveStreamObserver).onNext(any()); + when(this.grpcClientSettingsManager.getClientSettings(any())).thenReturn(Settings.newBuilder().getDefaultInstanceForType()); + + MessageExt messageExt1 = new MessageExt(); + String msgId1 = "msgId1"; + String popCk1 = "0 0 60000 0 0 broker 0 0 0"; + messageExt1.setTopic(TOPIC); + messageExt1.setMsgId(msgId1); + MessageAccessor.putProperty(messageExt1, MessageConst.PROPERTY_POP_CK, popCk1); + messageExt1.setBody("body1".getBytes()); + MessageExt messageExt2 = new MessageExt(); + String msgId2 = "msgId2"; + String popCk2 = "0 0 60000 0 0 broker 0 1 1000"; + messageExt2.setTopic(TOPIC); + messageExt2.setMsgId(msgId2); + MessageAccessor.putProperty(messageExt2, MessageConst.PROPERTY_POP_CK, popCk2); + messageExt2.setBody("body2".getBytes()); + PopResult popResult = new PopResult(PopStatus.FOUND, Arrays.asList(messageExt1, messageExt2)); + when(this.messagingProcessor.popMessage( + any(), + any(), + anyString(), + anyString(), + anyInt(), + anyLong(), + anyLong(), + anyInt(), + any(), + anyBoolean(), + any(), + isNull(), + anyLong())).thenReturn(CompletableFuture.completedFuture(popResult)); + ArgumentCaptor msgIdCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor receiptHandleCaptor = ArgumentCaptor.forClass(ReceiptHandle.class); + when(this.messagingProcessor.changeInvisibleTime( + any(), + receiptHandleCaptor.capture(), + msgIdCaptor.capture(), + anyString(), + anyString(), + anyLong())).thenReturn(CompletableFuture.completedFuture(new AckResult())); + + // normal + ProxyContext ctx = createContext(); + this.grpcChannelManager.createChannel(ctx, ctx.getClientID()); + ReceiveMessageRequest receiveMessageRequest = 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(); + this.receiveMessageActivity.receiveMessage(ctx, receiveMessageRequest, receiveStreamObserver); + verify(this.messagingProcessor, times(0)).changeInvisibleTime( + any(), + any(), + anyString(), + anyString(), + anyString(), + anyLong()); + + // abnormal + this.grpcChannelManager.removeChannel(ctx.getClientID()); + this.receiveMessageActivity.receiveMessage(ctx, receiveMessageRequest, receiveStreamObserver); + verify(this.messagingProcessor, times(2)).changeInvisibleTime( + any(), + any(), + anyString(), + anyString(), + anyString(), + anyLong()); + assertEquals(Arrays.asList(msgId1, msgId2), msgIdCaptor.getAllValues()); + assertEquals(Arrays.asList(popCk1, popCk2), receiptHandleCaptor.getAllValues().stream().map(ReceiptHandle::encode).collect(Collectors.toList())); + } + + @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 0000000..a717c78 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageResponseStreamWriterTest.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.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.doThrow; +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); + ReceiveMessageRequest receiveMessageRequest = 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(); + writer.writeAndComplete( + ProxyContext.create(), + receiveMessageRequest, + 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()); + + // case: fail to write response status at first step + doThrow(new RuntimeException()).when(streamObserver).onNext(any()); + writer.writeAndComplete( + ProxyContext.create(), + receiveMessageRequest, + popResult + ); + verify(this.messagingProcessor, times(3)) + .changeInvisibleTime(any(), any(), anyString(), anyString(), anyString(), anyLong()); + } + + @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 0000000..87824e5 --- /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/RecallMessageActivityTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/producer/RecallMessageActivityTest.java new file mode 100644 index 0000000..e42aead --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/producer/RecallMessageActivityTest.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.v2.producer; + +import apache.rocketmq.v2.Code; +import apache.rocketmq.v2.RecallMessageRequest; +import apache.rocketmq.v2.RecallMessageResponse; +import apache.rocketmq.v2.Resource; +import org.apache.rocketmq.proxy.common.ProxyException; +import org.apache.rocketmq.proxy.common.ProxyExceptionCode; +import org.apache.rocketmq.proxy.grpc.v2.BaseActivityTest; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; + +public class RecallMessageActivityTest extends BaseActivityTest { + private RecallMessageActivity recallMessageActivity; + + @Before + public void before() throws Throwable { + super.before(); + this.recallMessageActivity = + new RecallMessageActivity(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + } + + @Test + public void testRecallMessage_success() { + when(this.messagingProcessor.recallMessage(any(), any(), any(), anyLong())) + .thenReturn(CompletableFuture.completedFuture("msgId")); + + RecallMessageResponse response = this.recallMessageActivity.recallMessage( + createContext(), + RecallMessageRequest.newBuilder() + .setRecallHandle("handle") + .setTopic(Resource.newBuilder().setResourceNamespace("ns").setName("topic")) + .build() + ).join(); + + assertEquals(Code.OK, response.getStatus().getCode()); + assertEquals("msgId", response.getMessageId()); + } + + @Test + public void testRecallMessage_fail() { + CompletableFuture exceptionFuture = new CompletableFuture(); + when(this.messagingProcessor.recallMessage(any(), any(), any(), anyLong())).thenReturn(exceptionFuture); + exceptionFuture.completeExceptionally( + new ProxyException(ProxyExceptionCode.MESSAGE_PROPERTY_CONFLICT_WITH_TYPE, "info")); + + CompletionException exception = Assert.assertThrows(CompletionException.class, () -> { + this.recallMessageActivity.recallMessage( + createContext(), + RecallMessageRequest.newBuilder() + .setRecallHandle("handle") + .setTopic(Resource.newBuilder().setResourceNamespace("ns").setName("topic")) + .build() + ).join(); + }); + Assert.assertTrue(exception.getCause() instanceof ProxyException); + ProxyException cause = (ProxyException) exception.getCause(); + Assert.assertEquals(ProxyExceptionCode.MESSAGE_PROPERTY_CONFLICT_WITH_TYPE, cause.getCode()); + } +} 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 0000000..4882a5e --- /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 0000000..abbf824 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/route/RouteActivityTest.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.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()); + + // test queueData with 2 read queues, 2 write queues, and no permission, expect 2 no permission queues. + QueueData queueDataWith2R2WNoPerm = createQueueData(2, 2, 0); + List partitionWith2R2WNoPerm = this.routeActivity.genMessageQueueFromQueueData(queueDataWith2R2WNoPerm, GRPC_TOPIC, TopicMessageType.UNSPECIFIED, GRPC_BROKER); + assertEquals(2, partitionWith2R2WNoPerm.size()); + assertEquals(2, partitionWith2R2WNoPerm.stream().filter(a -> a.getAcceptMessageTypesValue(0) == MessageType.MESSAGE_TYPE_UNSPECIFIED.getNumber()).count()); + assertEquals(2, partitionWith2R2WNoPerm.stream().filter(a -> a.getPermission() == Permission.NONE).count()); + assertEquals(0, partitionWith2R2WNoPerm.stream().filter(a -> a.getPermission() == Permission.WRITE).count()); + assertEquals(0, partitionWith2R2WNoPerm.stream().filter(a -> a.getPermission() == Permission.READ_WRITE).count()); + assertEquals(0, partitionWith2R2WNoPerm.stream().filter(a -> a.getPermission() == Permission.READ).count()); + + // test queueData with 0 read queues, 0 write queues, and no permission, expect 1 no permission queue. + QueueData queueDataWith0R0WNoPerm = createQueueData(0, 0, 0); + List partitionWith0R0WNoPerm = this.routeActivity.genMessageQueueFromQueueData(queueDataWith0R0WNoPerm, GRPC_TOPIC, TopicMessageType.UNSPECIFIED, GRPC_BROKER); + assertEquals(1, partitionWith0R0WNoPerm.size()); + assertEquals(1, partitionWith0R0WNoPerm.stream().filter(a -> a.getAcceptMessageTypesValue(0) == MessageType.MESSAGE_TYPE_UNSPECIFIED.getNumber()).count()); + assertEquals(1, partitionWith0R0WNoPerm.stream().filter(a -> a.getPermission() == Permission.NONE).count()); + assertEquals(0, partitionWith0R0WNoPerm.stream().filter(a -> a.getPermission() == Permission.WRITE).count()); + assertEquals(0, partitionWith0R0WNoPerm.stream().filter(a -> a.getPermission() == Permission.READ_WRITE).count()); + assertEquals(0, partitionWith0R0WNoPerm.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 0000000..3f4d3a2 --- /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(), 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 0000000..072630e --- /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 0000000..9720938 --- /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.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 0000000..6729ef0 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/processor/ProducerProcessorTest.java @@ -0,0 +1,254 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.CompletionException; +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.producer.RecallMessageHandle; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.proxy.common.ProxyException; +import org.apache.rocketmq.proxy.common.ProxyExceptionCode; +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.Assert; +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.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.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(), + 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(), + 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()); + } + + @Test + public void testRecallMessage_notDelayMessage() { + when(metadataService.getTopicMessageType(any(), any())).thenReturn(TopicMessageType.NORMAL); + CompletionException exception = Assert.assertThrows(CompletionException.class, () -> { + producerProcessor.recallMessage(createContext(), TOPIC, "handle", 3000).join(); + }); + assertTrue(exception.getCause() instanceof ProxyException); + ProxyException cause = (ProxyException) exception.getCause(); + assertEquals(ProxyExceptionCode.MESSAGE_PROPERTY_CONFLICT_WITH_TYPE, cause.getCode()); + } + + @Test + public void testRecallMessage_invalidRecallHandle() { + when(metadataService.getTopicMessageType(any(), any())).thenReturn(TopicMessageType.DELAY); + CompletionException exception = Assert.assertThrows(CompletionException.class, () -> { + producerProcessor.recallMessage(createContext(), TOPIC, "handle", 3000).join(); + }); + assertTrue(exception.getCause() instanceof ProxyException); + ProxyException cause = (ProxyException) exception.getCause(); + assertEquals("recall handle is invalid", cause.getMessage()); + } + + @Test + public void testRecallMessage_success() { + when(metadataService.getTopicMessageType(any(), any())).thenReturn(TopicMessageType.DELAY); + when(this.messageService.recallMessage(any(), any(), any(), anyLong())) + .thenReturn(CompletableFuture.completedFuture("msgId")); + + String handle = RecallMessageHandle.HandleV1.buildHandle(TOPIC, "brokerName", "timestampStr", "whateverId"); + String msgId = producerProcessor.recallMessage(createContext(), TOPIC, handle, 3000).join(); + assertEquals("msgId", msgId); + } + + 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 0000000..769b10a --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/processor/TransactionProcessorTest.java @@ -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. + */ + +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(), anyString(), commitOrRollbackCaptor.capture(), anyBoolean(), anyString(), anyString())) + .thenReturn(new EndTransactionRequestData("brokerName", new EndTransactionRequestHeader())); + + this.transactionProcessor.endTransaction( + createContext(), + "topic", + "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 0000000..d504fdc --- /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 0000000..11dd6bc --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/activity/AbstractRemotingActivityTest.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.proxy.remoting.activity; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import java.util.concurrent.CompletableFuture; +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.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.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.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 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 0000000..1dfc7ce --- /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/RecallMessageActivityTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/activity/RecallMessageActivityTest.java new file mode 100644 index 0000000..7d64923 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/activity/RecallMessageActivityTest.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.proxy.remoting.activity; + +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import org.apache.rocketmq.common.attribute.TopicMessageType; +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.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.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.RecallMessageRequestHeader; +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 java.util.concurrent.CompletableFuture; + +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 RecallMessageActivityTest extends InitConfigTest { + private static final String TOPIC = "topic"; + private static final String GROUP = "group"; + private static final String BROKER_NAME = "brokerName"; + + private RecallMessageActivity recallMessageActivity; + @Mock + private MessagingProcessor messagingProcessor; + @Mock + private MetadataService metadataService; + + @Spy + private ChannelHandlerContext ctx = new SimpleChannelHandlerContext(new SimpleChannel(null, "1", "2")) { + @Override + public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) { + return null; + } + }; + + @Before + public void init() { + recallMessageActivity = new RecallMessageActivity(null, messagingProcessor); + when(messagingProcessor.getMetadataService()).thenReturn(metadataService); + } + + @Test + public void testRecallMessage_notDelayMessage() { + when(metadataService.getTopicMessageType(any(), eq(TOPIC))).thenReturn(TopicMessageType.NORMAL); + ProxyException exception = Assert.assertThrows(ProxyException.class, () -> { + recallMessageActivity.processRequest0(ctx, mockRequest(), null); + }); + Assert.assertEquals(ProxyExceptionCode.MESSAGE_PROPERTY_CONFLICT_WITH_TYPE, exception.getCode()); + } + + @Test + public void testRecallMessage_success() throws Exception { + when(metadataService.getTopicMessageType(any(), eq(TOPIC))).thenReturn(TopicMessageType.DELAY); + RemotingCommand request = mockRequest(); + RemotingCommand expectResponse = RemotingCommand.createResponseCommand(ResponseCode.SUCCESS, ""); + when(messagingProcessor.request(any(), eq(BROKER_NAME), eq(request), anyLong())) + .thenReturn(CompletableFuture.completedFuture(expectResponse)); + RemotingCommand response = recallMessageActivity.processRequest0(ctx, request, null); + Assert.assertNull(response); + verify(ctx, times(1)).writeAndFlush(eq(expectResponse)); + } + + private RemotingCommand mockRequest() { + RecallMessageRequestHeader requestHeader = new RecallMessageRequestHeader(); + requestHeader.setProducerGroup(GROUP); + requestHeader.setTopic(TOPIC); + requestHeader.setRecallHandle("handle"); + requestHeader.setBrokerName(BROKER_NAME); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.RECALL_MESSAGE, requestHeader); + request.makeCustomHeaderToNet(); + return request; + } +} 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 0000000..4b7589c --- /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 0000000..1122405 --- /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 0000000..d947fa5 --- /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 0000000..f57116f --- /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 0000000..4a417ea --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/protocol/http2proxy/Http2ProtocolProxyHandlerTest.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.remoting.protocol.http2proxy; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.haproxy.HAProxyMessageEncoder; +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.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.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 0000000..ca6fe90 --- /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 0000000..cdfc7f7 --- /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 0000000..7e4d25f --- /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 0000000..20ce2a1 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/service/message/LocalMessageServiceTest.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.proxy.service.message; + +import io.netty.channel.Channel; +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.CompletionException; +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.RecallMessageProcessor; +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.RecallMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.RecallMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageResponseHeader; +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; + +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 RecallMessageProcessor recallMessageProcessorMock; + @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.getRecallMessageProcessor()).thenReturn(recallMessageProcessorMock); + 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.processRequestAsync(Mockito.any(Channel.class), Mockito.argThat(argument -> { + boolean first = argument.getCode() == RequestCode.CHANGE_MESSAGE_INVISIBLETIME; + boolean second = argument.readCustomHeader() instanceof ChangeInvisibleTimeRequestHeader; + return first && second; + }), Mockito.any(Boolean.class))).thenReturn(CompletableFuture.completedFuture(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); + } + + @Test + public void testRecallMessage_success() throws Exception { + RecallMessageResponseHeader responseHeader = new RecallMessageResponseHeader(); + responseHeader.setMsgId("msgId"); + RemotingCommand response = RemotingCommand.createResponseCommandWithHeader(ResponseCode.SUCCESS, responseHeader); + Mockito.when(recallMessageProcessorMock.processRequest(Mockito.any(SimpleChannelHandlerContext.class), + Mockito.any())).thenReturn(response); + RecallMessageRequestHeader requestHeader = new RecallMessageRequestHeader(); + String msgId = localMessageService.recallMessage(proxyContext, "brokerName", requestHeader, 1000L).join(); + assertThat(msgId).isEqualTo("msgId"); + } + + @Test + public void testRecallMessage_fail() throws Exception { + RecallMessageResponseHeader responseHeader = new RecallMessageResponseHeader(); + RemotingCommand response = RemotingCommand.createResponseCommandWithHeader(ResponseCode.SLAVE_NOT_AVAILABLE, responseHeader); + Mockito.when(recallMessageProcessorMock.processRequest(Mockito.any(SimpleChannelHandlerContext.class), + Mockito.any())).thenReturn(response); + RecallMessageRequestHeader requestHeader = new RecallMessageRequestHeader(); + CompletionException exception = Assert.assertThrows(CompletionException.class, () -> { + localMessageService.recallMessage(proxyContext, "brokerName", requestHeader, 1000L).join(); + }); + Assert.assertTrue(exception.getCause() instanceof ProxyException); + } + + 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 0000000..5894f87 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/service/metadata/ClusterMetadataServiceTest.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.proxy.service.metadata; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import org.apache.rocketmq.common.MixAll; +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.proxy.service.route.MessageQueueView; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +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.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 ClusterMetadataServiceTest extends BaseServiceTest { + + private ClusterMetadataService clusterMetadataService; + + protected static final String BROKER2_ADDR = "127.0.0.2:10911"; + + @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); + + BrokerData brokerData2 = new BrokerData(); + brokerData2.setBrokerName("brokerName2"); + HashMap addrs = new HashMap<>(); + addrs.put(MixAll.MASTER_ID, BROKER2_ADDR); + brokerData2.setBrokerAddrs(addrs); + brokerData2.setCluster(CLUSTER_NAME); + topicRouteData.getBrokerDatas().add(brokerData2); + when(this.topicRouteService.getAllMessageQueueView(any(), eq(TOPIC))).thenReturn(new MessageQueueView(CLUSTER_NAME, topicRouteData, null)); + + } + + @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()); + } + + @Test + public void findOneBroker() { + + Set resultBrokerNames = new HashSet<>(); + // run 1000 times to test the random + for (int i = 0; i < 1000; i++) { + Optional brokerData = null; + try { + brokerData = this.clusterMetadataService.findOneBroker(TOPIC); + resultBrokerNames.add(brokerData.get().getBrokerName()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + // we should choose two brokers + assertEquals(2, resultBrokerNames.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 0000000..e2d05b0 --- /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 0000000..2cdd92b --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/service/mqclient/ProxyClientRemotingProcessorTest.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.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.common.MixAll; +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 { + // Temporarily skip this test on the Mac system as it is flaky + if (MixAll.isMac()) { + return; + } + CompletableFuture> proxyRelayResultFuture = new CompletableFuture<>(); + when(proxyRelayService.processCheckTransactionState(any(), any(), any(), any())) + .thenReturn(new RelayData<>( + new TransactionData("brokerName", "topic", 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(3)).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 0000000..a01c356 --- /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(3)).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 0000000..4ec797d --- /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 0000000..947ae2c --- /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 0000000..15d8348 --- /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 0000000..1ad39a1 --- /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 0000000..d150f87 --- /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 0000000..9a2c5e3 --- /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 0000000..e4f655b --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/service/transaction/AbstractTransactionServiceTest.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.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, + "Topic", + PRODUCER_GROUP, + RANDOM.nextLong(), + RANDOM.nextLong(), + txId, + message + ); + assertNotNull(transactionData); + + EndTransactionRequestData requestData = transactionService.genEndTransactionRequestHeader( + ctx, + "topic", + 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, + "topic", + "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, + "Topic", + PRODUCER_GROUP, + RANDOM.nextLong(), + RANDOM.nextLong(), + txId, + message + ); + transactionService.onSendCheckTransactionStateFailed(ProxyContext.createForInner(this.getClass()), PRODUCER_GROUP, transactionData); + assertNull(transactionService.genEndTransactionRequestHeader( + ctx, + "topic", + 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 0000000..91af74c --- /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 0000000..90a7f6b --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/service/transaction/TransactionDataManagerTest.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.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", + "topicName", + 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 0000000..ca6ee9c --- /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 0000000..0c0b28b --- /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 0000000..091a51f --- /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 0000000..f0873e2 --- /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 0000000..8695d52 --- /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 0000000..9f806be --- /dev/null +++ b/remoting/BUILD.bazel @@ -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. +# +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", + "@maven//:org_reflections_reflections", + ], +) + +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", + "@maven//:org_reflections_reflections", + ], + 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 new file mode 100644 index 0000000..9b91c64 --- /dev/null +++ b/remoting/pom.xml @@ -0,0 +1,54 @@ + + + + + org.apache.rocketmq + rocketmq-all + 5.3.4-SNAPSHOT + + + 4.0.0 + jar + rocketmq-remoting + rocketmq-remoting ${project.version} + + + ${basedir}/.. + + + + + ${project.groupId} + rocketmq-common + + + org.apache.commons + commons-lang3 + + + org.reflections + reflections + + + com.google.code.gson + gson + 2.9.0 + test + + + diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/ChannelEventListener.java b/remoting/src/main/java/org/apache/rocketmq/remoting/ChannelEventListener.java new file mode 100644 index 0000000..6802e69 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/ChannelEventListener.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.remoting; + +import io.netty.channel.Channel; + +public interface ChannelEventListener { + void onChannelConnect(final String remoteAddr, final Channel channel); + + void onChannelClose(final String remoteAddr, final Channel channel); + + 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/CommandCallback.java b/remoting/src/main/java/org/apache/rocketmq/remoting/CommandCallback.java new file mode 100644 index 0000000..884f3d9 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/CommandCallback.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.remoting; + +public interface CommandCallback { + + void accept(); +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/CommandCustomHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/CommandCustomHeader.java new file mode 100644 index 0000000..716fe9c --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/CommandCustomHeader.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; + +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public interface CommandCustomHeader { + void checkFields() throws RemotingCommandException; +} 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 0000000..5b3e5ca --- /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 new file mode 100644 index 0000000..6be4917 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/InvokeCallback.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; + +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 new file mode 100644 index 0000000..ebaeea4 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/RPCHook.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; + +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public interface RPCHook { + void doBeforeRequest(final String remoteAddr, final RemotingCommand request); + + void doAfterResponse(final String remoteAddr, final RemotingCommand request, + 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 new file mode 100644 index 0000000..c8389ee --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/RemotingClient.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; + +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 { + + void updateNameServerAddressList(final List addrs); + + List getNameServerAddressList(); + + List getAvailableNameSrvList(); + + RemotingCommand invokeSync(final String addr, final RemotingCommand request, + final long timeoutMillis) throws InterruptedException, RemotingConnectException, + RemotingSendRequestException, RemotingTimeoutException; + + void invokeAsync(final String addr, final RemotingCommand request, final long timeoutMillis, + final InvokeCallback invokeCallback) throws InterruptedException, RemotingConnectException, + RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException; + + void invokeOneway(final String addr, final RemotingCommand request, final long timeoutMillis) + 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 new file mode 100644 index 0000000..8cfa1e1 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/RemotingServer.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; + +import io.netty.channel.Channel; +import java.util.concurrent.ExecutorService; +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; +import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public interface RemotingServer extends RemotingService { + + void registerProcessor(final int requestCode, final NettyRequestProcessor processor, + final ExecutorService executor); + + void registerDefaultProcessor(final NettyRequestProcessor processor, final ExecutorService executor); + + int localListenPort(); + + 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; + + void invokeAsync(final Channel channel, final RemotingCommand request, final long timeoutMillis, + final InvokeCallback invokeCallback) throws InterruptedException, + RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException; + + void invokeOneway(final Channel channel, final RemotingCommand request, final long timeoutMillis) + throws InterruptedException, RemotingTooMuchRequestException, RemotingTimeoutException, + RemotingSendRequestException; + +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/RemotingService.java b/remoting/src/main/java/org/apache/rocketmq/remoting/RemotingService.java new file mode 100644 index 0000000..e7425ea --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/RemotingService.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; + +import org.apache.rocketmq.remoting.pipeline.RequestPipeline; + +public interface RemotingService { + void start(); + + void shutdown(); + + void registerRPCHook(RPCHook rpcHook); + + void setRequestPipeline(RequestPipeline pipeline); + + /** + * Remove all rpc hooks. + */ + void clearRPCHook(); +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/annotation/CFNotNull.java b/remoting/src/main/java/org/apache/rocketmq/remoting/annotation/CFNotNull.java new file mode 100644 index 0000000..9be208b --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/annotation/CFNotNull.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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE}) +public @interface CFNotNull { +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/annotation/CFNullable.java b/remoting/src/main/java/org/apache/rocketmq/remoting/annotation/CFNullable.java new file mode 100644 index 0000000..7ecc71f --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/annotation/CFNullable.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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE}) +public @interface CFNullable { +} 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 0000000..2401233 --- /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/RemotingHelper.java b/remoting/src/main/java/org/apache/rocketmq/remoting/common/RemotingHelper.java new file mode 100644 index 0000000..d94efe7 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/common/RemotingHelper.java @@ -0,0 +1,377 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 io.netty.util.Attribute; +import io.netty.util.AttributeKey; +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; +import java.util.concurrent.CompletableFuture; +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.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; + +public class RemotingHelper { + 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(LoggerName.ROCKETMQ_REMOTING_NAME); + + 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) { + } + } + }; + + 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) { + } + } + }; + + 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) { + 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, RemotingCommandException { + long beginTime = System.currentTimeMillis(); + SocketAddress socketAddress = NetworkUtil.string2SocketAddress(addr); + SocketChannel socketChannel = connect(socketAddress); + if (socketChannel != null) { + boolean sendRequestOK = false; + + try { + + socketChannel.configureBlocking(true); + + //bugfix http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4614802 + socketChannel.socket().setSoTimeout((int) timeoutMillis); + + ByteBuffer byteBufferRequest = request.encode(); + while (byteBufferRequest.hasRemaining()) { + int length = socketChannel.write(byteBufferRequest); + if (length > 0) { + if (byteBufferRequest.hasRemaining()) { + if ((System.currentTimeMillis() - beginTime) > timeoutMillis) { + + throw new RemotingSendRequestException(addr); + } + } + } else { + throw new RemotingSendRequestException(addr); + } + + Thread.sleep(1); + } + + sendRequestOK = true; + + ByteBuffer byteBufferSize = ByteBuffer.allocate(4); + while (byteBufferSize.hasRemaining()) { + int length = socketChannel.read(byteBufferSize); + if (length > 0) { + if (byteBufferSize.hasRemaining()) { + if ((System.currentTimeMillis() - beginTime) > timeoutMillis) { + + throw new RemotingTimeoutException(addr, timeoutMillis); + } + } + } else { + throw new RemotingTimeoutException(addr, timeoutMillis); + } + + Thread.sleep(1); + } + + int size = byteBufferSize.getInt(0); + ByteBuffer byteBufferBody = ByteBuffer.allocate(size); + while (byteBufferBody.hasRemaining()) { + int length = socketChannel.read(byteBufferBody); + if (length > 0) { + if (byteBufferBody.hasRemaining()) { + if ((System.currentTimeMillis() - beginTime) > timeoutMillis) { + + throw new RemotingTimeoutException(addr, timeoutMillis); + } + } + } else { + throw new RemotingTimeoutException(addr, timeoutMillis); + } + + Thread.sleep(1); + } + + byteBufferBody.flip(); + return RemotingCommand.decode(byteBufferBody); + } catch (IOException e) { + log.error("invokeSync failure", e); + + if (sendRequestOK) { + throw new RemotingTimeoutException(addr, timeoutMillis); + } else { + throw new RemotingSendRequestException(addr); + } + } finally { + try { + socketChannel.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } else { + throw new RemotingConnectException(addr); + } + } + + 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() : ""; + + if (addr.length() > 0) { + int index = addr.lastIndexOf("/"); + if (index >= 0) { + return addr.substring(index + 1); + } + + return addr; + } + + return ""; + } + + public static String parseChannelLocalAddr(final Channel channel) { + SocketAddress remote = channel.localAddress(); + final String addr = remote != null ? remote.toString() : ""; + + if (addr.length() > 0) { + int index = addr.lastIndexOf("/"); + if (index >= 0) { + return addr.substring(index + 1); + } + + return addr; + } + + 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); + } + + 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 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 CompletableFuture convertChannelFutureToCompletableFuture(ChannelFuture channelFuture) { + CompletableFuture completableFuture = new CompletableFuture<>(); + channelFuture.addListener((ChannelFutureListener) future -> { + if (future.isSuccess()) { + completableFuture.complete(null); + } else { + completableFuture.completeExceptionally(new RemotingConnectException(channelFuture.channel().remoteAddress().toString(), future.cause())); + } + }); + return completableFuture; + } + + 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/SemaphoreReleaseOnlyOnce.java b/remoting/src/main/java/org/apache/rocketmq/remoting/common/SemaphoreReleaseOnlyOnce.java new file mode 100644 index 0000000..eac52ba --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/common/SemaphoreReleaseOnlyOnce.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.common; + +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicBoolean; + +public class SemaphoreReleaseOnlyOnce { + private final AtomicBoolean released = new AtomicBoolean(false); + private final Semaphore semaphore; + + public SemaphoreReleaseOnlyOnce(Semaphore semaphore) { + this.semaphore = semaphore; + } + + public void release() { + if (this.semaphore != null) { + if (this.released.compareAndSet(false, true)) { + this.semaphore.release(); + } + } + } + + public Semaphore getSemaphore() { + return semaphore; + } +} 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 new file mode 100644 index 0000000..a4ee814 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/common/ServiceThread.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.common; + + +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(LoggerName.ROCKETMQ_REMOTING_NAME); + + private static final long JOIN_TIME = 90 * 1000; + protected final Thread thread; + protected volatile boolean hasNotified = false; + protected volatile boolean stopped = false; + + public ServiceThread() { + this.thread = new Thread(this, this.getServiceName()); + } + + public abstract String getServiceName(); + + public void start() { + this.thread.start(); + } + + public void shutdown() { + this.shutdown(false); + } + + public void shutdown(final boolean interrupt) { + this.stopped = true; + log.info("shutdown thread " + this.getServiceName() + " interrupt " + interrupt); + synchronized (this) { + if (!this.hasNotified) { + this.hasNotified = true; + this.notify(); + } + } + + try { + if (interrupt) { + this.thread.interrupt(); + } + + long beginTime = System.currentTimeMillis(); + this.thread.join(this.getJointime()); + long elapsedTime = System.currentTimeMillis() - beginTime; + log.info("join thread " + this.getServiceName() + " elapsed time(ms) " + elapsedTime + " " + + this.getJointime()); + } catch (InterruptedException e) { + log.error("Interrupted", e); + } + } + + public long getJointime() { + return JOIN_TIME; + } + + public boolean isStopped() { + return stopped; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/common/TlsMode.java b/remoting/src/main/java/org/apache/rocketmq/remoting/common/TlsMode.java new file mode 100644 index 0000000..996ef0d --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/common/TlsMode.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; + +/** + * For server, three SSL modes are supported: disabled, permissive and enforcing. + *
      + *
    1. disabled: SSL is not supported; any incoming SSL handshake will be rejected, causing connection closed.
    2. + *
    3. permissive: SSL is optional, aka, server in this mode can serve client connections with or without SSL;
    4. + *
    5. enforcing: SSL is required, aka, non SSL connection will be rejected.
    6. + *
    + */ +public enum TlsMode { + + DISABLED("disabled"), + PERMISSIVE("permissive"), + ENFORCING("enforcing"); + + private String name; + + TlsMode(String name) { + this.name = name; + } + + public static TlsMode parse(String mode) { + for (TlsMode tlsMode : TlsMode.values()) { + if (tlsMode.name.equals(mode)) { + return tlsMode; + } + } + + return PERMISSIVE; + } + + public String getName() { + return name; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/exception/RemotingCommandException.java b/remoting/src/main/java/org/apache/rocketmq/remoting/exception/RemotingCommandException.java new file mode 100644 index 0000000..e8e00c6 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/exception/RemotingCommandException.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.exception; + +public class RemotingCommandException extends RemotingException { + private static final long serialVersionUID = -6061365915274953096L; + + public RemotingCommandException(String message) { + super(message, null); + } + + public RemotingCommandException(String message, Throwable cause) { + super(message, cause); + } +} 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 new file mode 100644 index 0000000..40e3b5a --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/exception/RemotingConnectException.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.exception; + +public class RemotingConnectException extends RemotingException { + private static final long serialVersionUID = -5565366231695911316L; + + public RemotingConnectException(String addr) { + this(addr, null); + } + + public RemotingConnectException(String addr, Throwable cause) { + super("connect to " + addr + " failed", cause); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/exception/RemotingException.java b/remoting/src/main/java/org/apache/rocketmq/remoting/exception/RemotingException.java new file mode 100644 index 0000000..37929b0 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/exception/RemotingException.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.exception; + +public class RemotingException extends Exception { + private static final long serialVersionUID = -5690687334570505110L; + + public RemotingException(String message) { + super(message); + } + + public RemotingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/exception/RemotingSendRequestException.java b/remoting/src/main/java/org/apache/rocketmq/remoting/exception/RemotingSendRequestException.java new file mode 100644 index 0000000..6adefb8 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/exception/RemotingSendRequestException.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.exception; + +public class RemotingSendRequestException extends RemotingException { + private static final long serialVersionUID = 5391285827332471674L; + + public RemotingSendRequestException(String addr) { + this(addr, null); + } + + public RemotingSendRequestException(String addr, Throwable cause) { + super("send request to <" + addr + "> failed", cause); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/exception/RemotingTimeoutException.java b/remoting/src/main/java/org/apache/rocketmq/remoting/exception/RemotingTimeoutException.java new file mode 100644 index 0000000..1ca164d --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/exception/RemotingTimeoutException.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.exception; + +public class RemotingTimeoutException extends RemotingException { + + private static final long serialVersionUID = 4106899185095245979L; + + public RemotingTimeoutException(String message) { + super(message); + } + + public RemotingTimeoutException(String addr, long timeoutMillis) { + this(addr, timeoutMillis, null); + } + + public RemotingTimeoutException(String addr, long timeoutMillis, Throwable cause) { + super("wait response on the channel <" + addr + "> timeout, " + timeoutMillis + "(ms)", cause); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/exception/RemotingTooMuchRequestException.java b/remoting/src/main/java/org/apache/rocketmq/remoting/exception/RemotingTooMuchRequestException.java new file mode 100644 index 0000000..b10e134 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/exception/RemotingTooMuchRequestException.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.remoting.exception; + +public class RemotingTooMuchRequestException extends RemotingException { + private static final long serialVersionUID = 4326919581254519654L; + + public RemotingTooMuchRequestException(String message) { + super(message); + } +} 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 0000000..f9b3e4c --- /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 0000000..2e0d708 --- /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 0000000..ebdde31 --- /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 new file mode 100644 index 0000000..7373a56 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/FileRegionEncoder.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.remoting.netty; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.FileRegion; +import io.netty.handler.codec.MessageToByteEncoder; + +import io.netty.handler.ssl.SslHandler; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.WritableByteChannel; + +/** + *

    + * By default, file region are directly transferred to socket channel which is known as zero copy. In case we need + * to encrypt transmission, data being sent should go through the {@link SslHandler}. This encoder ensures this + * process. + *

    + */ +public class FileRegionEncoder extends MessageToByteEncoder { + + /** + * Encode a message into a {@link io.netty.buffer.ByteBuf}. This method will be called for each written message that + * can be handled by this encoder. + * + * @param ctx the {@link io.netty.channel.ChannelHandlerContext} which this {@link + * io.netty.handler.codec.MessageToByteEncoder} belongs to + * @param msg the message to encode + * @param out the {@link io.netty.buffer.ByteBuf} into which the encoded message will be written + * @throws Exception is thrown if an error occurs + */ + @Override + protected void encode(ChannelHandlerContext ctx, FileRegion msg, final ByteBuf out) throws Exception { + WritableByteChannel writableByteChannel = new WritableByteChannel() { + @Override + public int write(ByteBuffer src) { + int prev = out.writerIndex(); + out.writeBytes(src); + return out.writerIndex() - prev; + } + + @Override + public boolean isOpen() { + return true; + } + + @Override + public void close() throws IOException { + } + }; + + long toTransfer = msg.count(); + + while (true) { + long transferred = msg.transferred(); + if (toTransfer - transferred <= 0) { + break; + } + msg.transferTo(writableByteChannel, transferred); + } + } +} 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 new file mode 100644 index 0000000..8260163 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyClientConfig.java @@ -0,0 +1,221 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.common.TlsMode; + +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_ENABLE; + +public class NettyClientConfig { + /** + * Worker thread number + */ + 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 = NettySystemConfig.connectTimeoutMillis; + private long channelNotActiveInterval = 1000 * 60; + + private boolean isScanAvailableNameSrv = true; + + /** + * 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 = NettySystemConfig.clientChannelMaxIdleTimeSeconds; + + private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; + private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; + private boolean clientPooledByteBufAllocatorEnable = 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 disableCallbackExecutor = false; + private boolean disableNettyWorkerGroup = false; + + private long maxReconnectIntervalTimeSeconds = 60; + + private boolean enableReconnectForGoAway = true; + + public boolean isClientCloseSocketIfTimeout() { + return clientCloseSocketIfTimeout; + } + + public void setClientCloseSocketIfTimeout(final boolean clientCloseSocketIfTimeout) { + this.clientCloseSocketIfTimeout = clientCloseSocketIfTimeout; + } + + public int getClientWorkerThreads() { + return clientWorkerThreads; + } + + public void setClientWorkerThreads(int clientWorkerThreads) { + this.clientWorkerThreads = clientWorkerThreads; + } + + public int getClientOnewaySemaphoreValue() { + return clientOnewaySemaphoreValue; + } + + public void setClientOnewaySemaphoreValue(int clientOnewaySemaphoreValue) { + this.clientOnewaySemaphoreValue = clientOnewaySemaphoreValue; + } + + public int getConnectTimeoutMillis() { + return connectTimeoutMillis; + } + + public void setConnectTimeoutMillis(int connectTimeoutMillis) { + this.connectTimeoutMillis = connectTimeoutMillis; + } + + public int getClientCallbackExecutorThreads() { + return clientCallbackExecutorThreads; + } + + public void setClientCallbackExecutorThreads(int clientCallbackExecutorThreads) { + this.clientCallbackExecutorThreads = clientCallbackExecutorThreads; + } + + public long getChannelNotActiveInterval() { + return channelNotActiveInterval; + } + + public void setChannelNotActiveInterval(long channelNotActiveInterval) { + this.channelNotActiveInterval = channelNotActiveInterval; + } + + public int getClientAsyncSemaphoreValue() { + return clientAsyncSemaphoreValue; + } + + public void setClientAsyncSemaphoreValue(int clientAsyncSemaphoreValue) { + this.clientAsyncSemaphoreValue = clientAsyncSemaphoreValue; + } + + public int getClientChannelMaxIdleTimeSeconds() { + return clientChannelMaxIdleTimeSeconds; + } + + public void setClientChannelMaxIdleTimeSeconds(int clientChannelMaxIdleTimeSeconds) { + this.clientChannelMaxIdleTimeSeconds = clientChannelMaxIdleTimeSeconds; + } + + public int getClientSocketSndBufSize() { + return clientSocketSndBufSize; + } + + public void setClientSocketSndBufSize(int clientSocketSndBufSize) { + this.clientSocketSndBufSize = clientSocketSndBufSize; + } + + public int getClientSocketRcvBufSize() { + return clientSocketRcvBufSize; + } + + public void setClientSocketRcvBufSize(int clientSocketRcvBufSize) { + this.clientSocketRcvBufSize = clientSocketRcvBufSize; + } + + public boolean isClientPooledByteBufAllocatorEnable() { + return clientPooledByteBufAllocatorEnable; + } + + public void setClientPooledByteBufAllocatorEnable(boolean clientPooledByteBufAllocatorEnable) { + this.clientPooledByteBufAllocatorEnable = clientPooledByteBufAllocatorEnable; + } + + public boolean isUseTLS() { + return useTLS; + } + + 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 String getSocksProxyConfig() { + return socksProxyConfig; + } + + public void setSocksProxyConfig(String socksProxyConfig) { + this.socksProxyConfig = socksProxyConfig; + } + + public boolean isScanAvailableNameSrv() { + return isScanAvailableNameSrv; + } + + public void setScanAvailableNameSrv(boolean scanAvailableNameSrv) { + this.isScanAvailableNameSrv = scanAvailableNameSrv; + } +} 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 new file mode 100644 index 0000000..19624d7 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyDecoder.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.netty; + +import com.google.common.base.Stopwatch; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; +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.RemotingCommand; + +public class NettyDecoder extends LengthFieldBasedFrameDecoder { + 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")); + + public NettyDecoder() { + super(FRAME_MAX_LENGTH, 0, 4, 0, 4); + } + + @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; + } + RemotingCommand cmd = RemotingCommand.decode(frame); + cmd.setProcessTimer(timer); + return cmd; + } catch (Exception e) { + log.error("decode exception, " + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), e); + RemotingHelper.closeChannel(ctx.channel()); + } finally { + if (null != frame) { + frame.release(); + } + } + + return null; + } +} 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 new file mode 100644 index 0000000..2af0af6 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyEncoder.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.netty; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; +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.RemotingCommand; + +@ChannelHandler.Sharable +public class NettyEncoder extends MessageToByteEncoder { + private static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_REMOTING_NAME); + + @Override + public void encode(ChannelHandlerContext ctx, RemotingCommand remotingCommand, ByteBuf out) + throws Exception { + try { + remotingCommand.fastEncodeHeader(out); + byte[] body = remotingCommand.getBody(); + if (body != null) { + out.writeBytes(body); + } + } catch (Exception e) { + log.error("encode exception, " + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), e); + if (remotingCommand != null) { + log.error(remotingCommand.toString()); + } + RemotingHelper.closeChannel(ctx.channel()); + } + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyEvent.java b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyEvent.java new file mode 100644 index 0000000..9a1bc94 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyEvent.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.netty; + +import io.netty.channel.Channel; + +public class NettyEvent { + private final NettyEventType type; + private final String remoteAddr; + private final Channel channel; + + public NettyEvent(NettyEventType type, String remoteAddr, Channel channel) { + this.type = type; + this.remoteAddr = remoteAddr; + this.channel = channel; + } + + public NettyEventType getType() { + return type; + } + + public String getRemoteAddr() { + return remoteAddr; + } + + public Channel getChannel() { + return channel; + } + + @Override + public String toString() { + return "NettyEvent [type=" + type + ", remoteAddr=" + remoteAddr + ", channel=" + 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 new file mode 100644 index 0000000..4bc9d57 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyEventType.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.remoting.netty; + +public enum NettyEventType { + CONNECT, + CLOSE, + IDLE, + 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 0000000..0a2b2da --- /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.trace(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.trace(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.trace(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.trace(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.trace(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.trace(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.trace(var1); + } + + @Override + public void trace(String var1, Object var2) { + logger.trace(var1, var2); + } + + @Override + public void trace(String var1, Object var2, Object var3) { + logger.trace(var1, var2, var3); + } + + @Override + public void trace(String var1, Object... var2) { + logger.trace(var1, var2); + } + + @Override + public void trace(String var1, Throwable var2) { + logger.trace(var1, var2); + } + + @Override + public void trace(Throwable var1) { + logger.trace(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 new file mode 100644 index 0000000..a4f23f1 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingAbstract.java @@ -0,0 +1,737 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslHandler; +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.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +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.common.utils.ExceptionUtils; +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.RemotingHelper; +import org.apache.rocketmq.remoting.common.SemaphoreReleaseOnlyOnce; +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.pipeline.RequestPipeline; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RemotingSysResponseCode; +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(LoggerName.ROCKETMQ_REMOTING_NAME); + + /** + * Semaphore to limit maximum number of on-going one-way requests, which protects system memory footprint. + */ + protected final Semaphore semaphoreOneway; + + /** + * Semaphore to limit maximum number of on-going asynchronous requests, which protects system memory footprint. + */ + protected final Semaphore semaphoreAsync; + + /** + * This map caches all on-going requests. + */ + protected final ConcurrentMap responseTable = + 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); + + /** + * Executor to feed netty events to user defined {@link ChannelEventListener}. + */ + 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. + */ + protected Pair defaultRequestProcessorPair; + + /** + * SSL context via which to create {@link SslHandler}. + */ + protected volatile SslContext sslContext; + + /** + * custom rpc hooks + */ + protected List rpcHooks = new ArrayList<>(); + + protected RequestPipeline requestPipeline; + + 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. + */ + public NettyRemotingAbstract(final int permitsOneway, final int permitsAsync) { + this.semaphoreOneway = new Semaphore(permitsOneway, true); + this.semaphoreAsync = new Semaphore(permitsAsync, true); + } + + /** + * Custom channel event listener. + * + * @return custom channel event listener if defined; null otherwise. + */ + public abstract ChannelEventListener getChannelEventListener(); + + /** + * Put a netty event to the executor. + * + * @param event Netty event instance. + */ + public void putNettyEvent(final NettyEvent event) { + this.nettyEventExecutor.putNettyEvent(event); + } + + /** + * Entry of incoming command processing. + * + *

    + * Note: + * The incoming remoting command may be + *

      + *
    • An inquiry request from a remote peer component;
    • + *
    • A response to a previous request issued by this very participant.
    • + *
    + *

    + * + * @param ctx Channel handler context. + * @param msg incoming remoting command. + */ + public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand msg) { + if (msg != null) { + switch (msg.getType()) { + case REQUEST_COMMAND: + processRequestCommand(ctx, msg); + break; + case RESPONSE_COMMAND: + processResponseCommand(ctx, msg); + break; + default: + break; + } + } + } + + 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. + * + * @param ctx channel handler context. + * @param cmd request command. + */ + public void processRequestCommand(final ChannelHandlerContext ctx, final RemotingCommand cmd) { + final Pair matched = this.processorTable.get(cmd.getCode()); + final Pair pair = null == matched ? this.defaultRequestProcessorPair : matched; + final int opaque = cmd.getOpaque(); + + 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; + } + + Runnable run = buildProcessRequestHandler(ctx, cmd, pair, opaque); + + if (isShuttingDown.get()) { + if (cmd.getVersion() > MQVersion.Version.V5_3_1.ordinal()) { + final RemotingCommand response = RemotingCommand.createResponseCommand(ResponseCode.GO_AWAY, + "please go away"); + response.setOpaque(opaque); + writeResponse(ctx.channel(), cmd, response); + log.info("proxy is shutting down, write response GO_AWAY. channel={}, requestCode={}, opaque={}", ctx.channel(), cmd.getCode(), opaque); + 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; + String remoteAddr = null; + + try { + remoteAddr = RemotingHelper.parseChannelRemoteAddr(ctx.channel()); + try { + doBeforeRpcHooks(remoteAddr, cmd); + } catch (AbortProcessException e) { + throw e; + } catch (Exception e) { + exception = e; + } + + if (this.requestPipeline != null) { + this.requestPipeline.execute(ctx, cmd); + } + + 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, remoteAddr: {}", remoteAddr, e); + log.error(cmd.toString()); + + if (!cmd.isOnewayRPC()) { + response = RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_ERROR, + UtilAll.exceptionSimpleDesc(e)); + response.setOpaque(opaque); + writeResponse(ctx.channel(), cmd, response); + } + } + }; + } + + /** + * Process response from remote peer to the previous issued requests. + * + * @param ctx channel handler context. + * @param cmd response command instance. + */ + public void processResponseCommand(ChannelHandlerContext ctx, RemotingCommand cmd) { + final int opaque = cmd.getOpaque(); + final ResponseFuture responseFuture = responseTable.get(opaque); + if (responseFuture != null) { + responseFuture.setResponseCommand(cmd); + + responseTable.remove(opaque); + + if (responseFuture.getInvokeCallback() != null) { + executeInvokeCallback(responseFuture); + } else { + responseFuture.putResponse(cmd); + responseFuture.release(); + } + } else { + log.warn("receive response, cmd={}, but not matched any request, address={}, channelId={}", cmd, RemotingHelper.parseChannelRemoteAddr(ctx.channel()), ctx.channel().id()); + } + } + + /** + * Execute callback in callback executor. If callback executor is null, run directly in current thread + */ + private void executeInvokeCallback(final ResponseFuture responseFuture) { + boolean runInThisThread = false; + ExecutorService executor = this.getCallbackExecutor(); + if (executor != null && !executor.isShutdown()) { + try { + 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) { + runInThisThread = true; + log.warn("execute callback in executor exception, maybe executor busy", e); + } + } else { + runInThisThread = true; + } + + if (runInThisThread) { + try { + responseFuture.executeInvokeCallback(); + } catch (Throwable e) { + log.warn("executeInvokeCallback Exception", e); + } finally { + responseFuture.release(); + } + } + } + + /** + * Custom RPC hooks. + * + * @return RPC hooks if specified; null otherwise. + */ + public List getRPCHook() { + return rpcHooks; + } + + public void registerRPCHook(RPCHook rpcHook) { + if (rpcHook != null && !rpcHooks.contains(rpcHook)) { + rpcHooks.add(rpcHook); + } + } + + public void setRequestPipeline(RequestPipeline pipeline) { + this.requestPipeline = pipeline; + } + + public void clearRPCHook() { + rpcHooks.clear(); + } + + /** + * This method specifies thread pool to use while invoking callback methods. + * + * @return Dedicated thread pool instance if specified; or null if the callback is supposed to be executed in the + * netty event-loop thread. + */ + public abstract ExecutorService getCallbackExecutor(); + + /** + *

    + * This method is periodically invoked to scan and expire deprecated request. + *

    + */ + public void scanResponseTable() { + final List rfList = new LinkedList<>(); + Iterator> it = this.responseTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + ResponseFuture rep = next.getValue(); + + if ((rep.getBeginTimestamp() + rep.getTimeoutMillis() + 1000) <= System.currentTimeMillis()) { + rep.release(); + it.remove(); + rfList.add(rep); + log.warn("remove timeout request, " + rep); + } + } + + for (ResponseFuture rf : rfList) { + try { + executeInvokeCallback(rf); + } catch (Throwable e) { + log.warn("scanResponseTable, operationComplete Exception", e); + } + } + } + + public RemotingCommand invokeSyncImpl(final Channel channel, final RemotingCommand request, + final long timeoutMillis) + throws InterruptedException, RemotingSendRequestException, RemotingTimeoutException { + try { + 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()); + } + } + + public CompletableFuture invokeImpl(final Channel channel, final RemotingCommand request, + final long timeoutMillis) { + return invoke0(channel, request, timeoutMillis); + } + + 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; + 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; + } + + AtomicReference responseFutureReference = new AtomicReference<>(); + final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, request, timeoutMillis - costTime, + new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + + @Override + public void operationSucceed(RemotingCommand response) { + future.complete(responseFutureReference.get()); + } + + @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 <{}>, channelId={}, failed.", RemotingHelper.parseChannelRemoteAddr(channel), channel.id()); + }); + return future; + } catch (Exception e) { + responseTable.remove(opaque); + responseFuture.release(); + log.warn("send a request command to channel <{}> channelId={} Exception", RemotingHelper.parseChannelRemoteAddr(channel), channel.id(), e); + future.completeExceptionally(new RemotingSendRequestException(RemotingHelper.parseChannelRemoteAddr(channel), e)); + return future; + } + } else { + if (timeoutMillis <= 0) { + future.completeExceptionally(new RemotingTooMuchRequestException("invokeAsyncImpl invoke too fast")); + } else { + String info = + String.format("invokeAsyncImpl tryAcquire semaphore timeout, %dms, waiting thread nums: %d semaphoreAsyncValue: %d", + timeoutMillis, + this.semaphoreAsync.getQueueLength(), + this.semaphoreAsync.availablePermits() + ); + log.warn(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(ExceptionUtils.getRealException(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); + } + } + } + } + + public void invokeOnewayImpl(final Channel channel, final RemotingCommand request, final long timeoutMillis) + throws InterruptedException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException { + request.markOnewayRPC(); + boolean acquired = this.semaphoreOneway.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS); + if (acquired) { + final SemaphoreReleaseOnlyOnce once = new SemaphoreReleaseOnlyOnce(this.semaphoreOneway); + try { + 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) { + once.release(); + log.warn("write send a request command to channel <" + channel.remoteAddress() + "> failed."); + throw new RemotingSendRequestException(RemotingHelper.parseChannelRemoteAddr(channel), e); + } + } else { + if (timeoutMillis <= 0) { + throw new RemotingTooMuchRequestException("invokeOnewayImpl invoke too fast"); + } else { + String info = String.format( + "invokeOnewayImpl tryAcquire semaphore timeout, %dms, waiting thread nums: %d semaphoreOnewayValue: %d", + timeoutMillis, + this.semaphoreOneway.getQueueLength(), + this.semaphoreOneway.availablePermits() + ); + log.warn(info); + throw new RemotingTimeoutException(info); + } + } + } + + public HashMap> getProcessorTable() { + return processorTable; + } + + class NettyEventExecutor extends ServiceThread { + private final LinkedBlockingQueue eventQueue = new LinkedBlockingQueue<>(); + + public void putNettyEvent(final NettyEvent event) { + int currentSize = this.eventQueue.size(); + int maxSize = 10000; + if (currentSize <= maxSize) { + this.eventQueue.add(event); + } else { + log.warn("event queue size [{}] over the limit [{}], so drop this event {}", currentSize, maxSize, event.toString()); + } + } + + @Override + public void run() { + log.info(this.getServiceName() + " service started"); + + final ChannelEventListener listener = NettyRemotingAbstract.this.getChannelEventListener(); + + while (!this.isStopped()) { + try { + NettyEvent event = this.eventQueue.poll(3000, TimeUnit.MILLISECONDS); + if (event != null && listener != null) { + switch (event.getType()) { + case IDLE: + listener.onChannelIdle(event.getRemoteAddr(), event.getChannel()); + break; + case CLOSE: + listener.onChannelClose(event.getRemoteAddr(), event.getChannel()); + break; + case CONNECT: + listener.onChannelConnect(event.getRemoteAddr(), event.getChannel()); + break; + case EXCEPTION: + listener.onChannelException(event.getRemoteAddr(), event.getChannel()); + break; + case ACTIVE: + listener.onChannelActive(event.getRemoteAddr(), event.getChannel()); + break; + default: + break; + + } + } + } catch (Exception e) { + log.warn(this.getServiceName() + " service has exception. ", e); + } + } + + log.info(this.getServiceName() + " service end"); + } + + @Override + public String getServiceName() { + return NettyEventExecutor.class.getSimpleName(); + } + } +} 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 new file mode 100644 index 0000000..92ced6b --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingClient.java @@ -0,0 +1,1219 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 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; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +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.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 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.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.FutureUtils; +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.RemotingClient; +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.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.proxy.SocksProxyConfig; + +import static org.apache.rocketmq.remoting.common.RemotingHelper.convertChannelFutureToCompletableFuture; + +public class NettyRemotingClient extends NettyRemotingAbstract implements RemotingClient { + 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; + + protected final NettyClientConfig nettyClientConfig; + private final Bootstrap bootstrap = new Bootstrap(); + private final EventLoopGroup eventLoopGroupWorker; + private final Lock lockChannelTables = new ReentrantLock(); + 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 HashedWheelTimer timer = new HashedWheelTimer(r -> new Thread(r, "ClientHouseKeepingService")); + + 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 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 EventExecutorGroup defaultEventExecutorGroup; + + public NettyRemotingClient(final NettyClientConfig nettyClientConfig) { + this(nettyClientConfig, null); + } + + 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 ThreadFactoryImpl("NettyClientPublicExecutor_")); + + this.scanExecutor = ThreadUtils.newThreadPoolExecutor(4, 10, 60, TimeUnit.SECONDS, + new ArrayBlockingQueue<>(32), new ThreadFactoryImpl("NettyClientScan_thread_")); + + 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); + LOGGER.info("SSL enabled for client"); + } catch (IOException e) { + LOGGER.error("Failed to create SSLContext", e); + } catch (CertificateException e) { + LOGGER.error("Failed to create SSLContext", e); + throw new RuntimeException("Failed to create SSLContext", e); + } + } + } + + private static int initValueIndex() { + Random r = new Random(); + return r.nextInt(999); + } + + private void loadSocksProxyJson() { + Map sockProxyMap = JSON.parseObject( + nettyClientConfig.getSocksProxyConfig(), new TypeReference>() { + }); + if (sockProxyMap != null) { + proxyMap.putAll(sockProxyMap); + } + } + + @Override + public void start() { + 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()) + .handler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + 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!"); + } + } + ch.pipeline().addLast( + nettyClientConfig.isDisableNettyWorkerGroup() ? null : defaultEventExecutorGroup, + new NettyEncoder(), + new NettyDecoder(), + new IdleStateHandler(0, 0, nettyClientConfig.getClientChannelMaxIdleTimeSeconds()), + new NettyConnectManageHandler(), + 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); + } + + nettyEventExecutor.start(); + + TimerTask timerTaskScanResponseTable = new TimerTask() { + @Override + public void run(Timeout timeout) { + try { + NettyRemotingClient.this.scanResponseTable(); + } catch (Throwable e) { + LOGGER.error("scanResponseTable exception", e); + } finally { + timer.newTimeout(this, 1000, TimeUnit.MILLISECONDS); + } + } + }; + this.timer.newTimeout(timerTaskScanResponseTable, 1000 * 3, TimeUnit.MILLISECONDS); + + if (nettyClientConfig.isScanAvailableNameSrv()) { + 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; + } + + protected ChannelFuture doConnect(String addr) { + String[] hostAndPort = getHostAndPort(addr); + String host = hostAndPort[0]; + int port = Integer.parseInt(hostAndPort[1]); + return fetchBootstrap(addr).connect(host, port); + } + + 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()); + } + }); + + // Support Netty Socks5 Proxy + if (proxy != null) { + bootstrap.resolver(NoopAddressResolverGroup.INSTANCE); + } + return bootstrap; + } + + // Do not use RemotingHelper.string2SocketAddress(), it will directly resolve the domain + protected 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.stop(); + + for (Map.Entry channel : this.channelTables.entrySet()) { + channel.getValue().close(); + } + + this.channelWrapperTables.clear(); + this.channelTables.clear(); + + this.eventLoopGroupWorker.shutdownGracefully(); + + if (this.nettyEventExecutor != null) { + this.nettyEventExecutor.shutdown(); + } + + if (this.defaultEventExecutorGroup != null) { + this.defaultEventExecutorGroup.shutdownGracefully(); + } + } catch (Exception e) { + LOGGER.error("NettyRemotingClient shutdown exception, ", e); + } + + if (this.publicExecutor != null) { + try { + this.publicExecutor.shutdown(); + } catch (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) { + return; + } + + final String addrRemote = null == addr ? RemotingHelper.parseChannelRemoteAddr(channel) : addr; + + try { + if (this.lockChannelTables.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { + try { + boolean removeItemFromTable = true; + final ChannelWrapper prevCW = this.channelTables.get(addrRemote); + + LOGGER.info("closeChannel: begin close the channel[addr={}, id={}] Found: {}", addrRemote, channel.id(), prevCW != null); + + if (null == prevCW) { + LOGGER.info("closeChannel: the channel[addr={}, id={}] has been removed from the channel table before", addrRemote, channel.id()); + removeItemFromTable = false; + } else if (prevCW.isWrapperOf(channel)) { + LOGGER.info("closeChannel: the channel[addr={}, id={}] has been closed before, and has been created again, nothing to do.", + addrRemote, channel.id()); + removeItemFromTable = false; + } + + if (removeItemFromTable) { + ChannelWrapper channelWrapper = this.channelWrapperTables.remove(channel); + if (channelWrapper != null && channelWrapper.tryClose(channel)) { + this.channelTables.remove(addrRemote); + } + LOGGER.info("closeChannel: the channel[addr={}, id={}] was removed from channel table", addrRemote, channel.id()); + } + + RemotingHelper.closeChannel(channel); + } catch (Exception e) { + LOGGER.error("closeChannel: close the channel exception", e); + } finally { + this.lockChannelTables.unlock(); + } + } else { + LOGGER.warn("closeChannel: try to lock channel table, but timeout, {}ms", LOCK_TIMEOUT_MILLIS); + } + } catch (InterruptedException e) { + LOGGER.error("closeChannel exception", e); + } + } + + public void closeChannel(final Channel channel) { + if (null == channel) { + return; + } + + try { + if (this.lockChannelTables.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { + try { + boolean removeItemFromTable = true; + ChannelWrapper prevCW = null; + String addrRemote = null; + for (Map.Entry entry : channelTables.entrySet()) { + String key = entry.getKey(); + ChannelWrapper prev = entry.getValue(); + if (prev.isWrapperOf(channel)) { + prevCW = prev; + addrRemote = key; + break; + } + } + + if (null == prevCW) { + LOGGER.info("eventCloseChannel: the channel[addr={}, id={}] has been removed from the channel table before", RemotingHelper.parseChannelRemoteAddr(channel), channel.id()); + removeItemFromTable = false; + } + + if (removeItemFromTable) { + ChannelWrapper channelWrapper = this.channelWrapperTables.remove(channel); + if (channelWrapper != null && channelWrapper.tryClose(channel)) { + this.channelTables.remove(addrRemote); + } + LOGGER.info("closeChannel: the channel[addr={}, id={}] was removed from channel table", addrRemote, channel.id()); + RemotingHelper.closeChannel(channel); + } + } catch (Exception e) { + LOGGER.error("closeChannel: close the channel[id={}] exception", channel.id(), e); + } finally { + this.lockChannelTables.unlock(); + } + } else { + LOGGER.warn("closeChannel: try to lock channel table, but timeout, {}ms", LOCK_TIMEOUT_MILLIS); + } + } catch (InterruptedException e) { + LOGGER.error("closeChannel exception", e); + } + } + + @Override + public void updateNameServerAddressList(List addrs) { + List old = this.namesrvAddrList.get(); + boolean update = false; + + if (!addrs.isEmpty()) { + if (null == old) { + update = true; + } else if (addrs.size() != old.size()) { + update = true; + } else { + for (String addr : addrs) { + if (!old.contains(addr)) { + update = true; + break; + } + } + } + + if (update) { + Collections.shuffle(addrs); + LOGGER.info("name server address updated. NEW : {} , OLD: {}", addrs, old); + this.namesrvAddrList.set(addrs); + + // should close the channel if choosed addr is not exist. + String chosenNameServerAddr = this.namesrvAddrChoosed.get(); + if (chosenNameServerAddr != null && !addrs.contains(chosenNameServerAddr)) { + namesrvAddrChoosed.compareAndSet(chosenNameServerAddr, null); + for (String addr : this.channelTables.keySet()) { + if (addr.contains(chosenNameServerAddr)) { + ChannelWrapper channelWrapper = this.channelTables.get(addr); + if (channelWrapper != null) { + channelWrapper.close(); + } + } + } + } + } + } + } + + @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 { + 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) { + LOGGER.warn("invokeSync: send request exception, so close the channel[addr={}, id={}]", channelRemoteAddr, channel.id()); + this.closeChannel(addr, channel); + throw e; + } catch (RemotingTimeoutException e) { + // 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); + LOGGER.warn("invokeSync: close socket because of timeout, {}ms, channel[addr={}, id={}]", timeoutMillis, channelRemoteAddr, channel.id()); + } + LOGGER.warn("invokeSync: wait response timeout exception, the channel[addr={}, id={}]", channelRemoteAddr, channel.id()); + throw e; + } + } else { + this.closeChannel(addr, channel); + throw new RemotingConnectException(addr); + } + } + + @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() == RequestCode.PULL_MESSAGE || cmd.getCode() == RequestCode.LITE_PULL_MESSAGE)) { + 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 ChannelFuture getAndCreateChannelAsync(final String addr) throws InterruptedException { + if (null == addr) { + return getAndCreateNameserverChannelAsync(); + } + + ChannelWrapper cw = this.channelTables.get(addr); + if (cw != null && cw.isOK()) { + return cw.getChannelFuture(); + } + + return this.createChannelAsync(addr); + } + + private Channel getAndCreateChannel(final String addr) throws InterruptedException { + ChannelFuture channelFuture = getAndCreateChannelAsync(addr); + if (channelFuture == null) { + return null; + } + return channelFuture.awaitUninterruptibly().channel(); + } + + private ChannelFuture getAndCreateNameserverChannelAsync() throws InterruptedException { + String addr = this.namesrvAddrChoosed.get(); + if (addr != null) { + ChannelWrapper cw = this.channelTables.get(addr); + if (cw != null && cw.isOK()) { + return cw.getChannelFuture(); + } + } + + final List addrList = this.namesrvAddrList.get(); + if (this.namesrvChannelLock.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { + try { + addr = this.namesrvAddrChoosed.get(); + if (addr != null) { + ChannelWrapper cw = this.channelTables.get(addr); + if (cw != null && cw.isOK()) { + return cw.getChannelFuture(); + } + } + + if (addrList != null && !addrList.isEmpty()) { + int index = this.namesrvIndex.incrementAndGet(); + index = Math.abs(index); + index = index % addrList.size(); + String newAddr = addrList.get(index); + + this.namesrvAddrChoosed.set(newAddr); + LOGGER.info("new name server is chosen. OLD: {} , NEW: {}. namesrvIndex = {}", addr, newAddr, namesrvIndex); + return this.createChannelAsync(newAddr); + } + } catch (Exception e) { + LOGGER.error("getAndCreateNameserverChannel: create name server channel exception", e); + } finally { + this.namesrvChannelLock.unlock(); + } + } else { + LOGGER.warn("getAndCreateNameserverChannel: try to lock name server, but timeout, {}ms", LOCK_TIMEOUT_MILLIS); + } + + return null; + } + + private ChannelFuture createChannelAsync(final String addr) throws InterruptedException { + ChannelWrapper cw = this.channelTables.get(addr); + if (cw != null && cw.isOK()) { + return cw.getChannelFuture(); + } + + if (this.lockChannelTables.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { + try { + cw = this.channelTables.get(addr); + if (cw != null) { + if (cw.isOK() || !cw.getChannelFuture().isDone()) { + return cw.getChannelFuture(); + } else { + this.channelTables.remove(addr); + } + } + return createChannel(addr).getChannelFuture(); + } catch (Exception e) { + LOGGER.error("createChannel: create channel exception", e); + } finally { + this.lockChannelTables.unlock(); + } + } else { + LOGGER.warn("createChannel: try to lock channel table, but timeout, {}ms", LOCK_TIMEOUT_MILLIS); + } + + return null; + } + + private ChannelWrapper createChannel(String addr) { + ChannelFuture channelFuture = doConnect(addr); + LOGGER.info("createChannel: begin to connect remote host[{}] asynchronously", addr); + ChannelWrapper cw = new ChannelWrapper(addr, channelFuture); + this.channelTables.put(addr, cw); + this.channelWrapperTables.put(channelFuture.channel(), cw); + return cw; + } + + @Override + public void invokeAsync(String addr, RemotingCommand request, long timeoutMillis, InvokeCallback invokeCallback) + throws InterruptedException, RemotingConnectException, RemotingTooMuchRequestException, RemotingTimeoutException, + RemotingSendRequestException { + long beginStartTime = System.currentTimeMillis(); + final ChannelFuture channelFuture = this.getAndCreateChannelAsync(addr); + if (channelFuture == null) { + invokeCallback.operationFail(new RemotingConnectException(addr)); + return; + } + channelFuture.addListener(future -> { + if (future.isSuccess()) { + Channel channel = channelFuture.channel(); + String channelRemoteAddr = RemotingHelper.parseChannelRemoteAddr(channel); + if (channel != null && channel.isActive()) { + long costTime = System.currentTimeMillis() - beginStartTime; + if (timeoutMillis < costTime) { + invokeCallback.operationFail(new RemotingTooMuchRequestException("invokeAsync call the addr[" + channelRemoteAddr + "] timeout")); + } + this.invokeAsyncImpl(channel, request, timeoutMillis - costTime, new InvokeCallbackWrapper(invokeCallback, addr)); + } else { + this.closeChannel(addr, channel); + invokeCallback.operationFail(new RemotingConnectException(addr)); + } + } else { + invokeCallback.operationFail(new RemotingConnectException(addr)); + } + }); + } + + @Override + public void invokeOneway(String addr, RemotingCommand request, long timeoutMillis) throws InterruptedException, + RemotingConnectException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException { + final ChannelFuture channelFuture = this.getAndCreateChannelAsync(addr); + if (channelFuture == null) { + throw new RemotingConnectException(addr); + } + channelFuture.addListener(future -> { + if (future.isSuccess()) { + Channel channel = channelFuture.channel(); + String channelRemoteAddr = RemotingHelper.parseChannelRemoteAddr(channel); + if (channel != null && channel.isActive()) { + doBeforeRpcHooks(channelRemoteAddr, request); + this.invokeOnewayImpl(channel, request, timeoutMillis); + } else { + this.closeChannel(addr, channel); + } + } + }); + } + + @Override + public CompletableFuture invoke(String addr, RemotingCommand request, + long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + try { + final ChannelFuture channelFuture = this.getAndCreateChannelAsync(addr); + if (channelFuture == null) { + future.completeExceptionally(new RemotingConnectException(addr)); + return future; + } + channelFuture.addListener(f -> { + if (f.isSuccess()) { + Channel channel = channelFuture.channel(); + if (channel != null && channel.isActive()) { + invokeImpl(channel, request, timeoutMillis).whenComplete((v, t) -> { + if (t == null) { + updateChannelLastResponseTime(addr); + } + }).thenApply(ResponseFuture::getResponseCommand).whenComplete((v, t) -> { + if (t != null) { + future.completeExceptionally(t); + } else { + future.complete(v); + } + }); + } else { + this.closeChannel(addr, channel); + future.completeExceptionally(new RemotingConnectException(addr)); + } + } else { + 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(); + String channelRemoteAddr = RemotingHelper.parseChannelRemoteAddr(channel); + doBeforeRpcHooks(channelRemoteAddr, request); + + return super.invokeImpl(channel, request, timeoutMillis).thenCompose(responseFuture -> { + RemotingCommand response = responseFuture.getResponseCommand(); + if (response.getCode() == ResponseCode.GO_AWAY) { + if (nettyClientConfig.isEnableReconnectForGoAway()) { + LOGGER.info("Receive go away from channelId={}, channel={}", channel.id(), channel); + ChannelWrapper channelWrapper = channelWrapperTables.computeIfPresent(channel, (channel0, channelWrapper0) -> { + try { + if (channelWrapper0.reconnect(channel0)) { + LOGGER.info("Receive go away from channelId={}, channel={}, recreate the channelId={}", channel0.id(), channel0, channelWrapper0.getChannel().id()); + channelWrapperTables.put(channelWrapper0.getChannel(), channelWrapper0); + } + } catch (Throwable t) { + LOGGER.error("Channel {} reconnect error", channelWrapper0, t); + } + return channelWrapper0; + }); + if (channelWrapper != null && !channelWrapper.isWrapperOf(channel)) { + RemotingCommand retryRequest = RemotingCommand.createRequestCommand(request.getCode(), request.readCustomHeader()); + retryRequest.setBody(request.getBody()); + retryRequest.setExtFields(request.getExtFields()); + CompletableFuture future = convertChannelFutureToCompletableFuture(channelWrapper.getChannelFuture()); + return future.thenCompose(v -> { + long duration = stopwatch.elapsed(TimeUnit.MILLISECONDS); + stopwatch.stop(); + return super.invokeImpl(channelWrapper.getChannel(), retryRequest, timeoutMillis - duration) + .thenCompose(r -> { + if (r.getResponseCommand().getCode() == ResponseCode.GO_AWAY) { + return FutureUtils.completeExceptionally(new RemotingSendRequestException(channelRemoteAddr, + new Throwable("Receive GO_AWAY twice in request from channelId=" + channel.id()))); + } + return CompletableFuture.completedFuture(r); + }); + }); + } else { + LOGGER.warn("invokeImpl receive GO_AWAY, channelWrapper is null or channel is the same in wrapper, channelId={}", channel.id()); + } + } + return FutureUtils.completeExceptionally(new RemotingSendRequestException(channelRemoteAddr, new Throwable("Receive GO_AWAY from channelId=" + channel.id()))); + } + return CompletableFuture.completedFuture(responseFuture); + }).whenComplete((v, t) -> { + if (t == null) { + doAfterRpcHooks(channelRemoteAddr, request, v.getResponseCommand()); + } + }); + } + + @Override + public void registerProcessor(int requestCode, NettyRequestProcessor processor, ExecutorService executor) { + ExecutorService executorThis = executor; + if (null == executor) { + executorThis = this.publicExecutor; + } + + Pair pair = new Pair<>(processor, executorThis); + this.processorTable.put(requestCode, pair); + } + + @Override + public boolean isChannelWritable(String addr) { + ChannelWrapper cw = this.channelTables.get(addr); + if (cw != null && cw.isOK()) { + return cw.isWritable(); + } + 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 List getAvailableNameSrvList() { + return new ArrayList<>(this.availableNamesrvAddrMap.keySet()); + } + + @Override + public ChannelEventListener getChannelEventListener() { + return channelEventListener; + } + + @Override + public ExecutorService getCallbackExecutor() { + if (nettyClientConfig.isDisableCallbackExecutor()) { + return null; + } + return callbackExecutor != null ? callbackExecutor : publicExecutor; + } + + @Override + public void setCallbackExecutor(final ExecutorService callbackExecutor) { + this.callbackExecutor = callbackExecutor; + } + + protected void scanChannelTablesOfNameServer() { + List nameServerList = this.namesrvAddrList.get(); + if (nameServerList == null) { + LOGGER.warn("[SCAN] Addresses of name server is empty!"); + return; + } + + for (Map.Entry entry : this.channelTables.entrySet()) { + String addr = entry.getKey(); + ChannelWrapper channelWrapper = entry.getValue(); + 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); + } + } + }); + } + } + + 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 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 getChannel() != null && getChannel().isActive(); + } + + public boolean isWritable() { + return getChannel().isWritable(); + } + + public boolean isWrapperOf(Channel channel) { + return this.channelFuture.channel() != null && this.channelFuture.channel() == channel; + } + + private Channel getChannel() { + return getChannelFuture().channel(); + } + + public ChannelFuture getChannelFuture() { + 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(Channel channel) { + if (!isWrapperOf(channel)) { + LOGGER.warn("channelWrapper has reconnect, so do nothing, now channelId={}, input channelId={}",getChannel().id(), channel.id()); + return false; + } + if (lock.writeLock().tryLock()) { + try { + if (isWrapperOf(channel)) { + channelToClose = channelFuture; + channelFuture = doConnect(channelAddress); + return true; + } else { + LOGGER.warn("channelWrapper has reconnect, so do nothing, now channelId={}, input channelId={}",getChannel().id(), channel.id()); + } + } finally { + lock.writeLock().unlock(); + } + } else { + LOGGER.warn("channelWrapper reconnect try lock fail, now channelId={}", getChannel().id()); + } + 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); + } + } + + public class NettyClientHandler extends SimpleChannelInboundHandler { + @Override + protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception { + processMessageReceived(ctx, msg); + } + } + + public class NettyConnectManageHandler extends ChannelDuplexHandler { + @Override + public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, + ChannelPromise promise) throws Exception { + final String local = localAddress == null ? NetworkUtil.getLocalAddress() : RemotingHelper.parseSocketAddressAddr(localAddress); + final String remote = remoteAddress == null ? "UNKNOWN" : RemotingHelper.parseSocketAddressAddr(remoteAddress); + LOGGER.info("NETTY CLIENT PIPELINE: CONNECT {} => {}", local, remote); + + super.connect(ctx, remoteAddress, localAddress, promise); + + if (NettyRemotingClient.this.channelEventListener != null) { + NettyRemotingClient.this.putNettyEvent(new NettyEvent(NettyEventType.CONNECT, remote, ctx.channel())); + } + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + final String remoteAddress = RemotingHelper.parseChannelRemoteAddr(ctx.channel()); + LOGGER.info("NETTY CLIENT PIPELINE: ACTIVE, {}, channelId={}", remoteAddress, ctx.channel().id()); + 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()); + LOGGER.info("NETTY CLIENT PIPELINE: DISCONNECT {}", remoteAddress); + closeChannel(ctx.channel()); + super.disconnect(ctx, promise); + + if (NettyRemotingClient.this.channelEventListener != null) { + NettyRemotingClient.this.putNettyEvent(new NettyEvent(NettyEventType.CLOSE, remoteAddress, ctx.channel())); + } + } + + @Override + public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { + final String remoteAddress = RemotingHelper.parseChannelRemoteAddr(ctx.channel()); + LOGGER.info("NETTY CLIENT PIPELINE: CLOSE channel[addr={}, id={}]", remoteAddress, ctx.channel().id()); + 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[addr={}, id={}]", remoteAddress, ctx.channel().id()); + 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()); + LOGGER.warn("NETTY CLIENT PIPELINE: IDLE exception channel[addr={}, id={}]", remoteAddress, ctx.channel().id()); + closeChannel(ctx.channel()); + if (NettyRemotingClient.this.channelEventListener != null) { + NettyRemotingClient.this + .putNettyEvent(new NettyEvent(NettyEventType.IDLE, remoteAddress, ctx.channel())); + } + } + } + + ctx.fireUserEventTriggered(evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + final String remoteAddress = RemotingHelper.parseChannelRemoteAddr(ctx.channel()); + LOGGER.warn("NETTY CLIENT PIPELINE: exceptionCaught channel[addr={}, id={}]", remoteAddress, ctx.channel().id(), 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 new file mode 100644 index 0000000..7ed8044 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingServer.java @@ -0,0 +1,811 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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 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.RemotingServer; +import org.apache.rocketmq.remoting.common.RemotingHelper; +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 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.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class NettyRemotingServer extends NettyRemotingAbstract implements RemotingServer { + 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; + protected final EventLoopGroup eventLoopGroupSelector; + protected final EventLoopGroup eventLoopGroupBoss; + protected final NettyServerConfig nettyServerConfig; + + private final ExecutorService publicExecutor; + private final ScheduledExecutorService scheduledExecutorService; + private final ChannelEventListener channelEventListener; + + private final HashedWheelTimer timer = new HashedWheelTimer(r -> new Thread(r, "ServerHouseKeepingService")); + + private DefaultEventExecutorGroup defaultEventExecutorGroup; + + /** + * 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 + protected final TlsModeHandler tlsModeHandler = new TlsModeHandler(TlsSystemConfig.tlsMode); + protected final NettyEncoder encoder = new NettyEncoder(); + protected final NettyConnectManageHandler connectionManageHandler = new NettyConnectManageHandler(); + protected final NettyServerHandler serverHandler = new NettyServerHandler(); + protected final RemotingCodeDistributionHandler distributionHandler = new RemotingCodeDistributionHandler(); + + public NettyRemotingServer(final NettyServerConfig nettyServerConfig) { + this(nettyServerConfig, null); + } + + public NettyRemotingServer(final NettyServerConfig nettyServerConfig, + final ChannelEventListener channelEventListener) { + super(nettyServerConfig.getServerOnewaySemaphoreValue(), nettyServerConfig.getServerAsyncSemaphoreValue()); + this.serverBootstrap = new ServerBootstrap(); + this.nettyServerConfig = nettyServerConfig; + this.channelEventListener = channelEventListener; + + this.publicExecutor = buildPublicExecutor(nettyServerConfig); + this.scheduledExecutorService = buildScheduleExecutor(); + + this.eventLoopGroupBoss = buildEventLoopGroupBoss(); + this.eventLoopGroupSelector = buildEventLoopGroupSelector(); + + loadSslContext(); + } + + protected EventLoopGroup buildEventLoopGroupSelector() { + if (useEpoll()) { + return new EpollEventLoopGroup(nettyServerConfig.getServerSelectorThreads(), new ThreadFactoryImpl("NettyServerEPOLLSelector_")); + } else { + return new NioEventLoopGroup(nettyServerConfig.getServerSelectorThreads(), new ThreadFactoryImpl("NettyServerNIOSelector_")); + } + } + + protected EventLoopGroup buildEventLoopGroupBoss() { + if (useEpoll()) { + return new EpollEventLoopGroup(1, new ThreadFactoryImpl("NettyEPOLLBoss_")); + } else { + return new NioEventLoopGroup(1, new ThreadFactoryImpl("NettyNIOBoss_")); + } + } + + 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()); + + if (tlsMode != TlsMode.DISABLED) { + try { + sslContext = TlsHelper.buildSslContext(false); + log.info("SSLContext created for server"); + } catch (CertificateException | IOException e) { + log.error("Failed to create SSLContext for server", e); + } + } + } + + private boolean useEpoll() { + return NetworkUtil.isLinuxPlatform() + && nettyServerConfig.isUseEpollNativeSelector() + && Epoll.isAvailable(); + } + + protected void initServerBootstrap(ServerBootstrap serverBootstrap) { + 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 void initChannel(SocketChannel ch) { + configChannel(ch); + } + }); + + addCustomConfig(serverBootstrap); + } + + @Override + public void start() { + this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(nettyServerConfig.getServerWorkerThreads(), + new ThreadFactoryImpl("NettyServerCodecThread_")); + + initServerBootstrap(serverBootstrap); + + try { + ChannelFuture sync = serverBootstrap.bind().sync(); + InetSocketAddress addr = (InetSocketAddress) sync.channel().localAddress(); + 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(); + } + + TimerTask timerScanResponseTable = new TimerTask() { + @Override + public void run(Timeout timeout) { + try { + NettyRemotingServer.this.scanResponseTable(); + } catch (Throwable e) { + log.error("scanResponseTable exception", e); + } finally { + timer.newTimeout(this, 1000, TimeUnit.MILLISECONDS); + } + } + }; + 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(nettyServerConfig.isServerNettyWorkerGroupEnable() ? defaultEventExecutorGroup : null, + HANDSHAKE_HANDLER_NAME, new HandshakeHandler()) + .addLast(nettyServerConfig.isServerNettyWorkerGroupEnable() ? defaultEventExecutorGroup : null, + 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 (nettyServerConfig.isEnableShutdownGracefully() && isShuttingDown.compareAndSet(false, true)) { + Thread.sleep(Duration.ofSeconds(nettyServerConfig.getShutdownWaitTimeSeconds()).toMillis()); + } + + this.timer.stop(); + + this.eventLoopGroupBoss.shutdownGracefully(); + + this.eventLoopGroupSelector.shutdownGracefully(); + + this.nettyEventExecutor.shutdown(); + + if (this.defaultEventExecutorGroup != null) { + this.defaultEventExecutorGroup.shutdownGracefully(); + } + } catch (Exception e) { + log.error("NettyRemotingServer shutdown exception, ", e); + } + + if (this.publicExecutor != null) { + try { + this.publicExecutor.shutdown(); + } catch (Exception e) { + log.error("NettyRemotingServer shutdown exception, ", e); + } + } + } + + @Override + public void registerProcessor(int requestCode, NettyRequestProcessor processor, ExecutorService executor) { + ExecutorService executorThis = executor; + if (null == executor) { + executorThis = this.publicExecutor; + } + + Pair pair = new Pair<>(processor, executorThis); + this.processorTable.put(requestCode, pair); + } + + @Override + public void registerDefaultProcessor(NettyRequestProcessor processor, ExecutorService executor) { + this.defaultRequestProcessorPair = new Pair<>(processor, executor); + } + + @Override + public int localListenPort() { + return this.nettyServerConfig.getListenPort(); + } + + @Override + public Pair getProcessorPair(int requestCode) { + 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 { + return this.invokeSyncImpl(channel, request, timeoutMillis); + } + + @Override + public void invokeAsync(Channel channel, RemotingCommand request, long timeoutMillis, InvokeCallback invokeCallback) + throws InterruptedException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException { + this.invokeAsyncImpl(channel, request, timeoutMillis, invokeCallback); + } + + @Override + public void invokeOneway(Channel channel, RemotingCommand request, long timeoutMillis) throws InterruptedException, + RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException { + this.invokeOnewayImpl(channel, request, timeoutMillis); + } + + @Override + public ChannelEventListener getChannelEventListener() { + return channelEventListener; + } + + @Override + public ExecutorService getCallbackExecutor() { + return this.publicExecutor; + } + + 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; + + TlsModeHandler(TlsMode tlsMode) { + this.tlsMode = tlsMode; + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) { + + // 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 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, 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 an SSL connection but sslContext is null"); + } + break; + + default: + log.warn("Unknown TLS mode"); + break; + } + } else if (tlsMode == TlsMode.ENFORCING) { + ctx.close(); + log.warn("Clients intend to establish an insecure connection while this server is running in SSL enforcing mode"); + } + + try { + // Remove this handler + ctx.pipeline().remove(this); + } catch (NoSuchElementException e) { + log.error("Error while removing TlsModeHandler", e); + } + + // Hand over this message to the next . + ctx.fireChannelRead(msg.retain()); + } + } + + @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 + 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); + } + } + + @ChannelHandler.Sharable + public class NettyConnectManageHandler extends ChannelDuplexHandler { + @Override + public void channelRegistered(ChannelHandlerContext ctx) throws Exception { + final String remoteAddress = RemotingHelper.parseChannelRemoteAddr(ctx.channel()); + log.info("NETTY SERVER PIPELINE: channelRegistered {}", remoteAddress); + super.channelRegistered(ctx); + } + + @Override + public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { + final String remoteAddress = RemotingHelper.parseChannelRemoteAddr(ctx.channel()); + log.info("NETTY SERVER PIPELINE: channelUnregistered, the channel[{}]", remoteAddress); + super.channelUnregistered(ctx); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + final String remoteAddress = RemotingHelper.parseChannelRemoteAddr(ctx.channel()); + log.info("NETTY SERVER PIPELINE: channelActive, the channel[{}]", remoteAddress); + super.channelActive(ctx); + + if (NettyRemotingServer.this.channelEventListener != null) { + NettyRemotingServer.this.putNettyEvent(new NettyEvent(NettyEventType.CONNECT, remoteAddress, ctx.channel())); + } + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + final String remoteAddress = RemotingHelper.parseChannelRemoteAddr(ctx.channel()); + log.info("NETTY SERVER PIPELINE: channelInactive, the channel[{}]", remoteAddress); + super.channelInactive(ctx); + + if (NettyRemotingServer.this.channelEventListener != null) { + NettyRemotingServer.this.putNettyEvent(new NettyEvent(NettyEventType.CLOSE, remoteAddress, ctx.channel())); + } + } + + @Override + 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); + RemotingHelper.closeChannel(ctx.channel()); + if (NettyRemotingServer.this.channelEventListener != null) { + NettyRemotingServer.this + .putNettyEvent(new NettyEvent(NettyEventType.IDLE, remoteAddress, ctx.channel())); + } + } + } + + ctx.fireUserEventTriggered(evt); + } + + @Override + 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); + + if (NettyRemotingServer.this.channelEventListener != null) { + NettyRemotingServer.this.putNettyEvent(new NettyEvent(NettyEventType.EXCEPTION, remoteAddress, 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())) { + RemotingHelper.setPropertyToAttr(channel, AttributeKeys.PROXY_PROTOCOL_ADDR, msg.sourceAddress()); + } + if (msg.sourcePort() > 0) { + RemotingHelper.setPropertyToAttr(channel, AttributeKeys.PROXY_PROTOCOL_PORT, String.valueOf(msg.sourcePort())); + } + if (StringUtils.isNotBlank(msg.destinationAddress())) { + RemotingHelper.setPropertyToAttr(channel, AttributeKeys.PROXY_PROTOCOL_SERVER_ADDR, msg.destinationAddress()); + } + if (msg.destinationPort() > 0) { + RemotingHelper.setPropertyToAttr(channel, AttributeKeys.PROXY_PROTOCOL_SERVER_PORT, 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())); + RemotingHelper.setPropertyToAttr(channel, key, new String(valueBytes, CharsetUtil.UTF_8)); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRequestProcessor.java b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRequestProcessor.java new file mode 100644 index 0000000..040f768 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRequestProcessor.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.remoting.netty; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +/** + * Common remoting command processor + */ +public interface NettyRequestProcessor { + RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) + throws Exception; + + boolean rejectRequest(); +} 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 new file mode 100644 index 0000000..664dee8 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyServerConfig.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.remoting.netty; + +public class NettyServerConfig implements Cloneable { + + /** + * 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; + private int serverOnewaySemaphoreValue = 256; + private int serverAsyncSemaphoreValue = 64; + private int serverChannelMaxIdleTimeSeconds = 120; + + 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 serverNettyWorkerGroupEnable = true; + private boolean serverPooledByteBufAllocatorEnable = true; + + private boolean enableShutdownGracefully = false; + private int shutdownWaitTimeSeconds = 30; + + /** + * make install + * + * + * ../glibc-2.10.1/configure \ --prefix=/usr \ --with-headers=/usr/include \ + * --host=x86_64-linux-gnu \ --build=x86_64-pc-linux-gnu \ --without-gd + */ + private boolean useEpollNativeSelector = false; + + public String getBindAddress() { + return bindAddress; + } + + public void setBindAddress(String bindAddress) { + this.bindAddress = bindAddress; + } + + public int getListenPort() { + return listenPort; + } + + public void setListenPort(int listenPort) { + this.listenPort = listenPort; + } + + public int getServerWorkerThreads() { + return serverWorkerThreads; + } + + public void setServerWorkerThreads(int serverWorkerThreads) { + this.serverWorkerThreads = serverWorkerThreads; + } + + public int getServerSelectorThreads() { + return serverSelectorThreads; + } + + public void setServerSelectorThreads(int serverSelectorThreads) { + this.serverSelectorThreads = serverSelectorThreads; + } + + public int getServerOnewaySemaphoreValue() { + return serverOnewaySemaphoreValue; + } + + public void setServerOnewaySemaphoreValue(int serverOnewaySemaphoreValue) { + this.serverOnewaySemaphoreValue = serverOnewaySemaphoreValue; + } + + public int getServerCallbackExecutorThreads() { + return serverCallbackExecutorThreads; + } + + public void setServerCallbackExecutorThreads(int serverCallbackExecutorThreads) { + this.serverCallbackExecutorThreads = serverCallbackExecutorThreads; + } + + public int getServerAsyncSemaphoreValue() { + return serverAsyncSemaphoreValue; + } + + public void setServerAsyncSemaphoreValue(int serverAsyncSemaphoreValue) { + this.serverAsyncSemaphoreValue = serverAsyncSemaphoreValue; + } + + public int getServerChannelMaxIdleTimeSeconds() { + return serverChannelMaxIdleTimeSeconds; + } + + public void setServerChannelMaxIdleTimeSeconds(int serverChannelMaxIdleTimeSeconds) { + this.serverChannelMaxIdleTimeSeconds = serverChannelMaxIdleTimeSeconds; + } + + public int getServerSocketSndBufSize() { + return serverSocketSndBufSize; + } + + public void setServerSocketSndBufSize(int serverSocketSndBufSize) { + this.serverSocketSndBufSize = serverSocketSndBufSize; + } + + public int getServerSocketRcvBufSize() { + return serverSocketRcvBufSize; + } + + 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; + } + + public void setServerPooledByteBufAllocatorEnable(boolean serverPooledByteBufAllocatorEnable) { + this.serverPooledByteBufAllocatorEnable = serverPooledByteBufAllocatorEnable; + } + + public boolean isUseEpollNativeSelector() { + return useEpollNativeSelector; + } + + public void setUseEpollNativeSelector(boolean useEpollNativeSelector) { + this.useEpollNativeSelector = useEpollNativeSelector; + } + + @Override + public Object clone() throws CloneNotSupportedException { + return 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 isServerNettyWorkerGroupEnable() { + return serverNettyWorkerGroupEnable; + } + + public void setServerNettyWorkerGroupEnable(boolean serverNettyWorkerGroupEnable) { + this.serverNettyWorkerGroupEnable = serverNettyWorkerGroupEnable; + } + + 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 new file mode 100644 index 0000000..edad844 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettySystemConfig.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.netty; + +public class NettySystemConfig { + public static final String COM_ROCKETMQ_REMOTING_NETTY_POOLED_BYTE_BUF_ALLOCATOR_ENABLE = + "com.rocketmq.remoting.nettyPooledByteBufAllocatorEnable"; + public static final String COM_ROCKETMQ_REMOTING_SOCKET_SNDBUF_SIZE = + "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")); + public static final int CLIENT_ASYNC_SEMAPHORE_VALUE = // + Integer.parseInt(System.getProperty(COM_ROCKETMQ_REMOTING_CLIENT_ASYNC_SEMAPHORE_VALUE, "65535")); + 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, "0")); + public static int socketRcvbufSize = + 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 0000000..c6a97fe --- /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 0000000..7185f20 --- /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 new file mode 100644 index 0000000..57ed360 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/RequestTask.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.netty; + +import io.netty.channel.Channel; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public class RequestTask implements Runnable { + private final Runnable runnable; + private final long createTimestamp = System.currentTimeMillis(); + private final Channel channel; + private final RemotingCommand request; + private volatile boolean stopRun = false; + + public RequestTask(final Runnable runnable, final Channel channel, final RemotingCommand request) { + this.runnable = runnable; + this.channel = channel; + this.request = request; + } + + @Override + public int hashCode() { + int result = runnable != null ? runnable.hashCode() : 0; + result = 31 * result + (int) (getCreateTimestamp() ^ (getCreateTimestamp() >>> 32)); + result = 31 * result + (channel != null ? channel.hashCode() : 0); + result = 31 * result + (request != null ? request.hashCode() : 0); + result = 31 * result + (isStopRun() ? 1 : 0); + return result; + } + + @Override + public boolean equals(final Object o) { + if (this == o) + return true; + if (!(o instanceof RequestTask)) + return false; + + final RequestTask that = (RequestTask) o; + + if (getCreateTimestamp() != that.getCreateTimestamp()) + return false; + if (isStopRun() != that.isStopRun()) + return false; + if (channel != null ? !channel.equals(that.channel) : that.channel != null) + return false; + return request != null ? request.getOpaque() == that.request.getOpaque() : that.request == null; + + } + + public long getCreateTimestamp() { + return createTimestamp; + } + + public boolean isStopRun() { + return stopRun; + } + + public void setStopRun(final boolean stopRun) { + this.stopRun = stopRun; + } + + @Override + public void run() { + if (!this.stopRun) + this.runnable.run(); + } + + public void returnResponse(int code, String remark) { + final RemotingCommand response = RemotingCommand.createResponseCommand(code, remark); + response.setOpaque(request.getOpaque()); + this.channel.writeAndFlush(response); + } +} 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 new file mode 100644 index 0000000..0882818 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/ResponseFuture.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.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(); + private final CountDownLatch countDownLatch = new CountDownLatch(1); + + private final SemaphoreReleaseOnlyOnce once; + + private final AtomicBoolean executeCallbackOnlyOnce = new AtomicBoolean(false); + private volatile RemotingCommand responseCommand; + private volatile boolean sendRequestOK = true; + private volatile Throwable cause; + private volatile boolean interrupted = false; + + 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; + } + + 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(); + } + } + + public boolean isTimeout() { + long diff = System.currentTimeMillis() - this.beginTimestamp; + return diff > this.timeoutMillis; + } + + public RemotingCommand waitResponse(final long timeoutMillis) throws InterruptedException { + this.countDownLatch.await(timeoutMillis, TimeUnit.MILLISECONDS); + return this.responseCommand; + } + + public void putResponse(final RemotingCommand responseCommand) { + this.responseCommand = responseCommand; + this.countDownLatch.countDown(); + } + + public long getBeginTimestamp() { + return beginTimestamp; + } + + public boolean isSendRequestOK() { + return sendRequestOK; + } + + public void setSendRequestOK(boolean sendRequestOK) { + this.sendRequestOK = sendRequestOK; + } + + public long getTimeoutMillis() { + return timeoutMillis; + } + + public InvokeCallback getInvokeCallback() { + return invokeCallback; + } + + public Throwable getCause() { + return cause; + } + + public void setCause(Throwable cause) { + this.cause = cause; + } + + public RemotingCommand getResponseCommand() { + return responseCommand; + } + + public void setResponseCommand(RemotingCommand responseCommand) { + this.responseCommand = responseCommand; + } + + 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 + + ", cause=" + cause + ", opaque=" + opaque + ", timeoutMillis=" + timeoutMillis + + ", invokeCallback=" + invokeCallback + ", beginTimestamp=" + beginTimestamp + + ", countDownLatch=" + countDownLatch + "]"; + } +} 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 new file mode 100644 index 0000000..d7a8dfb --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/TlsHelper.java @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.io.InputStream; +import java.security.cert.CertificateException; +import java.util.Properties; +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; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_CLIENT_KEYPASSWORD; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_CLIENT_KEYPATH; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_CLIENT_TRUSTCERTPATH; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_SERVER_AUTHCLIENT; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_SERVER_CERTPATH; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_SERVER_KEYPASSWORD; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_SERVER_KEYPATH; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_SERVER_NEED_CLIENT_AUTH; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_SERVER_TRUSTCERTPATH; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_TEST_MODE_ENABLE; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.tlsClientAuthServer; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.tlsClientCertPath; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.tlsClientKeyPassword; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.tlsClientKeyPath; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.tlsClientTrustCertPath; +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 TlsHelper { + + public interface DecryptionStrategy { + /** + * Decrypt the target encrpted private key file. + * + * @param privateKeyEncryptPath A pathname string + * @param forClient tells whether it's a client-side key file + * @return An input stream for a decrypted key file + * @throws IOException if an I/O error has occurred + */ + InputStream decryptPrivateKey(String privateKeyEncryptPath, boolean forClient) throws IOException; + } + + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.ROCKETMQ_REMOTING_NAME); + + private static DecryptionStrategy decryptionStrategy = new DecryptionStrategy() { + @Override + public InputStream decryptPrivateKey(final String privateKeyEncryptPath, + final boolean forClient) throws IOException { + return new FileInputStream(privateKeyEncryptPath); + } + }; + + + public static void registerDecryptionStrategy(final DecryptionStrategy decryptionStrategy) { + TlsHelper.decryptionStrategy = decryptionStrategy; + } + + public static SslContext buildSslContext(boolean forClient) throws IOException, CertificateException { + File configFile = new File(TlsSystemConfig.tlsConfigFile); + extractTlsConfigFromFile(configFile); + logTheFinalUsedTlsConfig(); + + SslProvider provider; + if (OpenSsl.isAvailable()) { + provider = SslProvider.OPENSSL; + LOGGER.info("Using OpenSSL provider"); + } else { + provider = SslProvider.JDK; + LOGGER.info("Using JDK SSL provider"); + } + + if (forClient) { + if (tlsTestModeEnable) { + return SslContextBuilder + .forClient() + .sslProvider(SslProvider.JDK) + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .build(); + } else { + SslContextBuilder sslContextBuilder = SslContextBuilder.forClient().sslProvider(SslProvider.JDK); + + + if (!tlsClientAuthServer) { + sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE); + } else { + if (!isNullOrEmpty(tlsClientTrustCertPath)) { + sslContextBuilder.trustManager(new File(tlsClientTrustCertPath)); + } + } + + return sslContextBuilder.keyManager( + !isNullOrEmpty(tlsClientCertPath) ? new FileInputStream(tlsClientCertPath) : null, + !isNullOrEmpty(tlsClientKeyPath) ? decryptionStrategy.decryptPrivateKey(tlsClientKeyPath, true) : null, + !isNullOrEmpty(tlsClientKeyPassword) ? tlsClientKeyPassword : null) + .build(); + } + } else { + + if (tlsTestModeEnable) { + SelfSignedCertificate selfSignedCertificate = new SelfSignedCertificate(); + return SslContextBuilder + .forServer(selfSignedCertificate.certificate(), selfSignedCertificate.privateKey()) + .sslProvider(provider) + .clientAuth(ClientAuth.OPTIONAL) + .build(); + } else { + SslContextBuilder sslContextBuilder = SslContextBuilder.forServer( + !isNullOrEmpty(tlsServerCertPath) ? new FileInputStream(tlsServerCertPath) : null, + !isNullOrEmpty(tlsServerKeyPath) ? decryptionStrategy.decryptPrivateKey(tlsServerKeyPath, false) : null, + !isNullOrEmpty(tlsServerKeyPassword) ? tlsServerKeyPassword : null) + .sslProvider(provider); + + if (!tlsServerAuthClient) { + sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE); + } else { + if (!isNullOrEmpty(tlsServerTrustCertPath)) { + sslContextBuilder.trustManager(new File(tlsServerTrustCertPath)); + } + } + + sslContextBuilder.clientAuth(parseClientAuthMode(tlsServerNeedClientAuth)); + return sslContextBuilder.build(); + } + } + } + + private static void extractTlsConfigFromFile(final File configFile) { + if (!(configFile.exists() && configFile.isFile() && configFile.canRead())) { + LOGGER.info("Tls config file doesn't exist, skip it"); + return; + } + + Properties properties; + properties = new Properties(); + InputStream inputStream = null; + try { + inputStream = new FileInputStream(configFile); + properties.load(inputStream); + } catch (IOException ignore) { + } finally { + if (null != inputStream) { + try { + inputStream.close(); + } catch (IOException ignore) { + } + } + } + + tlsTestModeEnable = Boolean.parseBoolean(properties.getProperty(TLS_TEST_MODE_ENABLE, String.valueOf(tlsTestModeEnable))); + tlsServerNeedClientAuth = properties.getProperty(TLS_SERVER_NEED_CLIENT_AUTH, tlsServerNeedClientAuth); + tlsServerKeyPath = properties.getProperty(TLS_SERVER_KEYPATH, tlsServerKeyPath); + tlsServerKeyPassword = properties.getProperty(TLS_SERVER_KEYPASSWORD, tlsServerKeyPassword); + tlsServerCertPath = properties.getProperty(TLS_SERVER_CERTPATH, tlsServerCertPath); + tlsServerAuthClient = Boolean.parseBoolean(properties.getProperty(TLS_SERVER_AUTHCLIENT, String.valueOf(tlsServerAuthClient))); + tlsServerTrustCertPath = properties.getProperty(TLS_SERVER_TRUSTCERTPATH, tlsServerTrustCertPath); + + tlsClientKeyPath = properties.getProperty(TLS_CLIENT_KEYPATH, tlsClientKeyPath); + tlsClientKeyPassword = properties.getProperty(TLS_CLIENT_KEYPASSWORD, tlsClientKeyPassword); + tlsClientCertPath = properties.getProperty(TLS_CLIENT_CERTPATH, tlsClientCertPath); + tlsClientAuthServer = Boolean.parseBoolean(properties.getProperty(TLS_CLIENT_AUTHSERVER, String.valueOf(tlsClientAuthServer))); + tlsClientTrustCertPath = properties.getProperty(TLS_CLIENT_TRUSTCERTPATH, tlsClientTrustCertPath); + } + + private static void logTheFinalUsedTlsConfig() { + LOGGER.info("Log the final used tls related configuration"); + LOGGER.info("{} = {}", TLS_TEST_MODE_ENABLE, tlsTestModeEnable); + LOGGER.debug("{} = {}", TLS_SERVER_NEED_CLIENT_AUTH, tlsServerNeedClientAuth); + LOGGER.debug("{} = {}", TLS_SERVER_KEYPATH, tlsServerKeyPath); + LOGGER.debug("{} = {}", TLS_SERVER_CERTPATH, tlsServerCertPath); + LOGGER.debug("{} = {}", TLS_SERVER_AUTHCLIENT, tlsServerAuthClient); + LOGGER.debug("{} = {}", TLS_SERVER_TRUSTCERTPATH, tlsServerTrustCertPath); + + LOGGER.debug("{} = {}", TLS_CLIENT_KEYPATH, tlsClientKeyPath); + LOGGER.debug("{} = {}", TLS_CLIENT_CERTPATH, tlsClientCertPath); + LOGGER.debug("{} = {}", TLS_CLIENT_AUTHSERVER, tlsClientAuthServer); + LOGGER.debug("{} = {}", TLS_CLIENT_TRUSTCERTPATH, tlsClientTrustCertPath); + } + + private static ClientAuth parseClientAuthMode(String authMode) { + if (null == authMode || authMode.trim().isEmpty()) { + return ClientAuth.NONE; + } + + String authModeUpper = authMode.toUpperCase(); + for (ClientAuth clientAuth : ClientAuth.values()) { + if (clientAuth.name().equals(authModeUpper)) { + return clientAuth; + } + } + + return ClientAuth.NONE; + } + + /** + * Determine if a string is {@code null} or {@link String#isEmpty()} returns {@code true}. + */ + private static boolean isNullOrEmpty(String s) { + return s == null || s.isEmpty(); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/TlsSystemConfig.java b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/TlsSystemConfig.java new file mode 100644 index 0000000..403bd6c --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/TlsSystemConfig.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.remoting.netty; + +import io.netty.handler.ssl.SslContext; +import org.apache.rocketmq.remoting.common.TlsMode; + +public class TlsSystemConfig { + public static final String TLS_SERVER_MODE = "tls.server.mode"; + public static final String TLS_ENABLE = "tls.enable"; + public static final String TLS_CONFIG_FILE = "tls.config.file"; + public static final String TLS_TEST_MODE_ENABLE = "tls.test.mode.enable"; + + public static final String TLS_SERVER_NEED_CLIENT_AUTH = "tls.server.need.client.auth"; + public static final String TLS_SERVER_KEYPATH = "tls.server.keyPath"; + public static final String TLS_SERVER_KEYPASSWORD = "tls.server.keyPassword"; + public static final String TLS_SERVER_CERTPATH = "tls.server.certPath"; + public static final String TLS_SERVER_AUTHCLIENT = "tls.server.authClient"; + public static final String TLS_SERVER_TRUSTCERTPATH = "tls.server.trustCertPath"; + + public static final String TLS_CLIENT_KEYPATH = "tls.client.keyPath"; + public static final String TLS_CLIENT_KEYPASSWORD = "tls.client.keyPassword"; + public static final String TLS_CLIENT_CERTPATH = "tls.client.certPath"; + public static final String TLS_CLIENT_AUTHSERVER = "tls.client.authServer"; + public static final String TLS_CLIENT_TRUSTCERTPATH = "tls.client.trustCertPath"; + + + /** + * To determine whether use SSL in client-side, include SDK client and BrokerOuterAPI + */ + public static boolean tlsEnable = Boolean.parseBoolean(System.getProperty(TLS_ENABLE, "false")); + + /** + * To determine whether use test mode when initialize TLS context + */ + public static boolean tlsTestModeEnable = Boolean.parseBoolean(System.getProperty(TLS_TEST_MODE_ENABLE, "true")); + + /** + * Indicates the state of the {@link javax.net.ssl.SSLEngine} with respect to client authentication. + * This configuration item really only applies when building the server-side {@link SslContext}, + * and can be set to none, require or optional. + */ + public static String tlsServerNeedClientAuth = System.getProperty(TLS_SERVER_NEED_CLIENT_AUTH, "none"); + /** + * The store path of server-side private key + */ + public static String tlsServerKeyPath = System.getProperty(TLS_SERVER_KEYPATH, null); + + /** + * The password of the server-side private key + */ + public static String tlsServerKeyPassword = System.getProperty(TLS_SERVER_KEYPASSWORD, null); + + /** + * The store path of server-side X.509 certificate chain in PEM format + */ + public static String tlsServerCertPath = System.getProperty(TLS_SERVER_CERTPATH, null); + + /** + * To determine whether verify the client endpoint's certificate strictly + */ + public static boolean tlsServerAuthClient = Boolean.parseBoolean(System.getProperty(TLS_SERVER_AUTHCLIENT, "false")); + + /** + * The store path of trusted certificates for verifying the client endpoint's certificate + */ + public static String tlsServerTrustCertPath = System.getProperty(TLS_SERVER_TRUSTCERTPATH, null); + + /** + * The store path of client-side private key + */ + public static String tlsClientKeyPath = System.getProperty(TLS_CLIENT_KEYPATH, null); + + /** + * The password of the client-side private key + */ + public static String tlsClientKeyPassword = System.getProperty(TLS_CLIENT_KEYPASSWORD, null); + + /** + * The store path of client-side X.509 certificate chain in PEM format + */ + public static String tlsClientCertPath = System.getProperty(TLS_CLIENT_CERTPATH, null); + + /** + * To determine whether verify the server endpoint's certificate strictly + */ + public static boolean tlsClientAuthServer = Boolean.parseBoolean(System.getProperty(TLS_CLIENT_AUTHSERVER, "false")); + + /** + * The store path of trusted certificates for verifying the server endpoint's certificate + */ + public static String tlsClientTrustCertPath = System.getProperty(TLS_CLIENT_TRUSTCERTPATH, null); + + /** + * For server, three SSL modes are supported: disabled, permissive and enforcing. + * For client, use {@link TlsSystemConfig#tlsEnable} to determine whether use SSL. + *
      + *
    1. disabled: SSL is not supported; any incoming SSL handshake will be rejected, causing connection closed.
    2. + *
    3. permissive: SSL is optional, aka, server in this mode can serve client connections with or without SSL;
    4. + *
    5. enforcing: SSL is required, aka, non SSL connection will be rejected.
    6. + *
    + */ + public static TlsMode tlsMode = TlsMode.parse(System.getProperty(TLS_SERVER_MODE, "permissive")); + + /** + * A config file to store the above TLS related configurations, + * except {@link TlsSystemConfig#tlsMode} and {@link TlsSystemConfig#tlsEnable} + */ + public static String tlsConfigFile = System.getProperty(TLS_CONFIG_FILE, "/etc/rocketmq/tls.properties"); +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/pipeline/RequestPipeline.java b/remoting/src/main/java/org/apache/rocketmq/remoting/pipeline/RequestPipeline.java new file mode 100644 index 0000000..575f0ef --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/pipeline/RequestPipeline.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.pipeline; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public interface RequestPipeline { + + void execute(ChannelHandlerContext ctx, RemotingCommand request) throws Exception; + + default RequestPipeline pipe(RequestPipeline source) { + return (ctx, request) -> { + source.execute(ctx, request); + execute(ctx, request); + }; + } +} 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 0000000..8f53c02 --- /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 0000000..9340a70 --- /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 0000000..655cf88 --- /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 0000000..4ff8176 --- /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 0000000..ebf9930 --- /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 0000000..7c561f5 --- /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 broadcasting mode + */ + int BROADCASTING_DISABLE_FORBIDDEN = 4; + /** + * 5=forbidden for a subscription(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 new file mode 100644 index 0000000..cf43cba --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/LanguageCode.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.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), + DOTNET((byte) 2), + PYTHON((byte) 3), + DELPHI((byte) 4), + ERLANG((byte) 5), + RUBY((byte) 6), + OTHER((byte) 7), + HTTP((byte) 8), + GO((byte) 9), + PHP((byte) 10), + OMS((byte) 11), + RUST((byte) 12), + NODE_JS((byte) 13); + + private byte code; + + LanguageCode(byte code) { + this.code = code; + } + + public static LanguageCode valueOf(byte code) { + for (LanguageCode languageCode : LanguageCode.values()) { + if (languageCode.getCode() == code) { + return languageCode; + } + } + return null; + } + + 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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/MQProtosHelper.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/MQProtosHelper.java new file mode 100644 index 0000000..918377f --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/MQProtosHelper.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; + +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.header.namesrv.RegisterBrokerRequestHeader; + +public class MQProtosHelper { + private static final Logger log = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); + + public static boolean registerBrokerToNameServer(final String nsaddr, final String brokerAddr, + final long timeoutMillis) { + RegisterBrokerRequestHeader requestHeader = new RegisterBrokerRequestHeader(); + requestHeader.setBrokerAddr(brokerAddr); + + RemotingCommand request = + RemotingCommand.createRequestCommand(RequestCode.REGISTER_BROKER, requestHeader); + + try { + RemotingCommand response = RemotingHelper.invokeSync(nsaddr, request, timeoutMillis); + if (response != null) { + return ResponseCode.SUCCESS == response.getCode(); + } + } catch (Exception e) { + log.error("Failed to register broker", e); + } + + return false; + } +} 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 0000000..074a089 --- /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; + } + + public 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 new file mode 100644 index 0000000..9b2b0f0 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RemotingCommand.java @@ -0,0 +1,653 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.List; +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.CommandCallback; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +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"; + 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<>(); + 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 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(); + private static final String INTEGER_CANONICAL_NAME_1 = Integer.class.getCanonicalName(); + private static final String INTEGER_CANONICAL_NAME_2 = int.class.getCanonicalName(); + private static final String LONG_CANONICAL_NAME_1 = Long.class.getCanonicalName(); + 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); + + private static SerializeType serializeTypeConfigInThisServer = SerializeType.JSON; + + static { + final String protocol = System.getProperty(SERIALIZE_TYPE_PROPERTY, System.getenv(SERIALIZE_TYPE_ENV)); + if (!StringUtils.isBlank(protocol)) { + try { + serializeTypeConfigInThisServer = SerializeType.valueOf(protocol); + } catch (IllegalArgumentException e) { + throw new RuntimeException("parser specified protocol error. protocol=" + protocol, e); + } + } + } + + private int code; + private LanguageCode language = LanguageCode.JAVA; + private int version = 0; + private int opaque = requestId.getAndIncrement(); + private int flag = 0; + 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; + private transient List callbackList; + + protected RemotingCommand() { + } + + public static RemotingCommand createRequestCommand(int code, CommandCustomHeader customHeader) { + RemotingCommand cmd = new RemotingCommand(); + cmd.setCode(code); + cmd.customHeader = customHeader; + setCmdVersion(cmd); + return 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 { + String v = System.getProperty(REMOTING_VERSION_KEY); + if (v != null) { + int value = Integer.parseInt(v); + cmd.setVersion(value); + configVersion = value; + } + } + } + + public static RemotingCommand createResponseCommand(Class classHeader) { + return createResponseCommand(RemotingSysResponseCode.SYSTEM_ERROR, "not set any response code", classHeader); + } + + public static RemotingCommand buildErrorResponse(int code, String remark, + 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(); + cmd.markResponseType(); + cmd.setCode(code); + cmd.setRemark(remark); + setCmdVersion(cmd); + + if (classHeader != null) { + try { + 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; + } + } + + return cmd; + } + + public static RemotingCommand createResponseCommand(int code, String remark) { + return createResponseCommand(code, remark, null); + } + + public static RemotingCommand decode(final byte[] array) throws RemotingCommandException { + ByteBuffer byteBuffer = ByteBuffer.wrap(array); + return decode(byteBuffer); + } + + public static RemotingCommand decode(final ByteBuffer byteBuffer) throws RemotingCommandException { + return decode(Unpooled.wrappedBuffer(byteBuffer)); + } + + 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(byteBuffer, headerLength, getProtocolType(oriHeaderLen)); + + int bodyLength = length - 4 - headerLength; + byte[] bodyData = null; + if (bodyLength > 0) { + bodyData = new byte[bodyLength]; + byteBuffer.readBytes(bodyData); + } + cmd.body = bodyData; + + return cmd; + } + + public static int getHeaderLength(int length) { + return length & 0xFFFFFF; + } + + 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(byteBuffer, len); + resultRMQ.setSerializeTypeCurrentRPC(type); + return resultRMQ; + default: + break; + } + + return null; + } + + public static SerializeType getProtocolType(int source) { + return SerializeType.valueOf((byte) ((source >> 24) & 0xFF)); + } + + public static int createNewRequestId() { + return requestId.getAndIncrement(); + } + + public static SerializeType getSerializeTypeConfigInThisServer() { + return serializeTypeConfigInThisServer; + } + + public static int markProtocolType(int source, SerializeType type) { + return (type.getCode() << 24) | (source & 0x00FFFFFF); + } + + public void markResponseType() { + int bits = 1 << RPC_TYPE; + this.flag |= bits; + } + + public CommandCustomHeader readCustomHeader() { + return customHeader; + } + + public void writeCustomHeader(CommandCustomHeader customHeader) { + this.customHeader = customHeader; + } + + public T decodeCommandCustomHeader( + Class classHeader) throws RemotingCommandException { + return decodeCommandCustomHeader(classHeader, false); + } + + public T decodeCommandCustomHeader( + Class classHeader, boolean isCached) throws RemotingCommandException { + if (isCached && cachedHeader != null) { + return classHeader.cast(cachedHeader); + } + cachedHeader = decodeCommandCustomHeaderDirectly(classHeader, true); + if (cachedHeader == null) { + return null; + } + return classHeader.cast(cachedHeader); + } + + public T decodeCommandCustomHeaderDirectly(Class classHeader, + boolean useFastEncode) throws RemotingCommandException { + T objectHeader; + try { + objectHeader = classHeader.getDeclaredConstructor().newInstance(); + } catch (Exception 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) { + if (!Modifier.isStatic(field.getModifiers())) { + String fieldName = field.getName(); + if (!fieldName.startsWith("this")) { + try { + String value = this.extFields.get(fieldName); + if (null == value) { + if (!isFieldNullable(field)) { + throw new RemotingCommandException("the custom field <" + fieldName + "> is null"); + } + continue; + } + + field.setAccessible(true); + String type = getCanonicalName(field.getType()); + Object valueParsed; + + if (type.equals(STRING_CANONICAL_NAME)) { + valueParsed = value; + } else if (type.equals(INTEGER_CANONICAL_NAME_1) || type.equals(INTEGER_CANONICAL_NAME_2)) { + valueParsed = Integer.parseInt(value); + } else if (type.equals(LONG_CANONICAL_NAME_1) || type.equals(LONG_CANONICAL_NAME_2)) { + valueParsed = Long.parseLong(value); + } else if (type.equals(BOOLEAN_CANONICAL_NAME_1) || type.equals(BOOLEAN_CANONICAL_NAME_2)) { + 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"); + } + + field.set(objectHeader, valueParsed); + + } catch (Throwable e) { + log.error("Failed field [{}] decoding", fieldName, e); + } + } + } + } + + objectHeader.checkFields(); + } + + return objectHeader; + } + + //make it able to test + Field[] getClazzFields(Class classHeader) { + Field[] field = CLASS_HASH_MAP.get(classHeader); + + if (field == null) { + 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); + } + } + return field; + } + + private boolean isFieldNullable(Field field) { + if (!NULLABLE_FIELD_CACHE.containsKey(field)) { + Annotation annotation = field.getAnnotation(CFNotNull.class); + synchronized (NULLABLE_FIELD_CACHE) { + NULLABLE_FIELD_CACHE.put(field, annotation == null); + } + } + return NULLABLE_FIELD_CACHE.get(field); + } + + private String getCanonicalName(Class clazz) { + String name = CANONICAL_NAME_CACHE.get(clazz); + + if (name == null) { + name = clazz.getCanonicalName(); + synchronized (CANONICAL_NAME_CACHE) { + CANONICAL_NAME_CACHE.put(clazz, name); + } + } + return name; + } + + public ByteBuffer encode() { + // 1> header length size + int length = 4; + + // 2> header data length + byte[] headerData = this.headerEncode(); + length += headerData.length; + + // 3> body data length + if (this.body != null) { + length += body.length; + } + + ByteBuffer result = ByteBuffer.allocate(4 + length); + + // length + result.putInt(length); + + // header length + result.putInt(markProtocolType(headerData.length, serializeTypeCurrentRPC)); + + // header data + result.put(headerData); + + // body data; + if (this.body != null) { + result.put(this.body); + } + + result.flip(); + + return result; + } + + private byte[] headerEncode() { + this.makeCustomHeaderToNet(); + if (SerializeType.ROCKETMQ == serializeTypeCurrentRPC) { + return RocketMQSerializable.rocketMQProtocolEncode(this); + } else { + return RemotingSerializable.encode(this); + } + } + + public void makeCustomHeaderToNet() { + if (this.customHeader != null) { + Field[] fields = getClazzFields(customHeader.getClass()); + if (null == this.extFields) { + this.extFields = new HashMap<>(); + } + + 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(this.customHeader); + } catch (Exception e) { + log.error("Failed to access field [{}]", name, e); + } + + if (value != null) { + this.extFields.put(name, value.toString()); + } + } + } + } + } + } + + 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); + } + + public ByteBuffer encodeHeader(final int bodyLength) { + // 1> header length size + int length = 4; + + // 2> header data length + byte[] headerData; + headerData = this.headerEncode(); + + length += headerData.length; + + // 3> body data length + length += bodyLength; + + ByteBuffer result = ByteBuffer.allocate(4 + length - bodyLength); + + // length + result.putInt(length); + + // header length + result.putInt(markProtocolType(headerData.length, serializeTypeCurrentRPC)); + + // header data + result.put(headerData); + + ((Buffer) result).flip(); + + return result; + } + + public void markOnewayRPC() { + int bits = 1 << RPC_ONEWAY; + this.flag |= bits; + } + + @JSONField(serialize = false) + public boolean isOnewayRPC() { + int bits = 1 << RPC_ONEWAY; + return (this.flag & bits) == bits; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + @JSONField(serialize = false) + public RemotingCommandType getType() { + if (this.isResponseType()) { + return RemotingCommandType.RESPONSE_COMMAND; + } + + return RemotingCommandType.REQUEST_COMMAND; + } + + @JSONField(serialize = false) + public boolean isResponseType() { + int bits = 1 << RPC_TYPE; + return (this.flag & bits) == bits; + } + + 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 int getOpaque() { + return opaque; + } + + public void setOpaque(int opaque) { + this.opaque = opaque; + } + + public int getFlag() { + return flag; + } + + public void setFlag(int flag) { + this.flag = flag; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public byte[] getBody() { + return body; + } + + 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; + } + + public void setExtFields(HashMap extFields) { + this.extFields = extFields; + } + + public void addExtField(String key, String value) { + if (null == extFields) { + 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)=" + + Integer.toBinaryString(flag) + ", remark=" + remark + ", extFields=" + extFields + ", serializeTypeCurrentRPC=" + + serializeTypeCurrentRPC + "]"; + } + + public SerializeType getSerializeTypeCurrentRPC() { + return serializeTypeCurrentRPC; + } + + public void setSerializeTypeCurrentRPC(SerializeType serializeTypeCurrentRPC) { + this.serializeTypeCurrentRPC = serializeTypeCurrentRPC; + } + + public Stopwatch getProcessTimer() { + return processTimer; + } + + public void setProcessTimer(Stopwatch processTimer) { + this.processTimer = processTimer; + } + + public List getCallbackList() { + return callbackList; + } + + public void setCallbackList(List callbackList) { + this.callbackList = callbackList; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RemotingCommandType.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RemotingCommandType.java new file mode 100644 index 0000000..01c853b --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RemotingCommandType.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.remoting.protocol; + +public enum RemotingCommandType { + REQUEST_COMMAND, + RESPONSE_COMMAND; +} 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 new file mode 100644 index 0000000..139a704 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RemotingSerializable.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.remoting.protocol; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.serializer.SerializerFeature; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public abstract class RemotingSerializable { + private final static Charset CHARSET_UTF8 = StandardCharsets.UTF_8; + + public static byte[] encode(final Object obj) { + if (obj == null) { + return null; + } + final String json = toJson(obj, false); + return json.getBytes(CHARSET_UTF8); + } + + public static String toJson(final Object obj, boolean prettyFormat) { + return JSON.toJSONString(obj, prettyFormat); + } + + public static T decode(final byte[] data, Class classOfT) { + if (data == null) { + return null; + } + return fromJson(data, classOfT); + } + + public static List decodeList(final byte[] data, Class classOfT) { + if (data == null) { + return null; + } + String json = new String(data, CHARSET_UTF8); + return JSON.parseArray(json, 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) { + return json.getBytes(CHARSET_UTF8); + } + 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); + } + + public String toJson(final boolean prettyFormat) { + return toJson(this, prettyFormat); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RemotingSysResponseCode.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RemotingSysResponseCode.java new file mode 100644 index 0000000..ad7cd28 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RemotingSysResponseCode.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.remoting.protocol; + +public class RemotingSysResponseCode { + + public static final int SUCCESS = 0; + + public static final int SYSTEM_ERROR = 1; + + public static final int SYSTEM_BUSY = 2; + + public static final int REQUEST_CODE_NOT_SUPPORTED = 3; + + public static final int TRANSACTION_FAILED = 4; +} 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 0000000..8b2749e --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RequestCode.java @@ -0,0 +1,300 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 UPDATE_AND_CREATE_TOPIC_LIST = 18; + 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 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 POP_ROLLBACK = 200056; + + 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 UPDATE_AND_CREATE_SUBSCRIPTIONGROUP_LIST = 225; + + 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_ALL_PRODUCER_INFO = 328; + + public static final int DELETE_EXPIRED_COMMITLOG = 329; + + 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 CHECK_ROCKSDB_CQ_WRITE_PROGRESS = 354; + public static final int EXPORT_ROCKSDB_CONFIG_TO_JSON = 355; + + public static final int LITE_PULL_MESSAGE = 361; + public static final int RECALL_MESSAGE = 370; + + 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; + + /** + * 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; + + public static final int AUTH_CREATE_USER = 3001; + public static final int AUTH_UPDATE_USER = 3002; + public static final int AUTH_DELETE_USER = 3003; + public static final int AUTH_GET_USER = 3004; + public static final int AUTH_LIST_USER = 3005; + + public static final int AUTH_CREATE_ACL = 3006; + public static final int AUTH_UPDATE_ACL = 3007; + public static final int AUTH_DELETE_ACL = 3008; + public static final int AUTH_GET_ACL = 3009; + public static final int AUTH_LIST_ACL = 3010; +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RequestHeaderRegistry.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RequestHeaderRegistry.java new file mode 100644 index 0000000..082827b --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RequestHeaderRegistry.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; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.reflections.Reflections; +import org.reflections.scanners.SubTypesScanner; +import org.reflections.util.ClasspathHelper; +import org.reflections.util.ConfigurationBuilder; + +public class RequestHeaderRegistry { + + private static final String PACKAGE_NAME = "org.apache.rocketmq.remoting.protocol.header"; + + private final Map> requestHeaderMap = new HashMap<>(); + + public static RequestHeaderRegistry getInstance() { + return RequestHeaderRegistryHolder.INSTANCE; + } + + public void initialize() { + Reflections reflections = new Reflections(new ConfigurationBuilder() + .setUrls(ClasspathHelper.forPackage(PACKAGE_NAME)) + .setScanners(new SubTypesScanner(false))); + + Set> classes = reflections.getSubTypesOf(CommandCustomHeader.class); + + classes.forEach(this::registerHeader); + } + + public Class getRequestHeader(int requestCode) { + return this.requestHeaderMap.get(requestCode); + } + + private void registerHeader(Class clazz) { + if (!clazz.isAnnotationPresent(RocketMQAction.class)) { + return; + } + RocketMQAction action = clazz.getAnnotation(RocketMQAction.class); + this.requestHeaderMap.putIfAbsent(action.value(), clazz); + } + + private static class RequestHeaderRegistryHolder { + private static final RequestHeaderRegistry INSTANCE = new RequestHeaderRegistry(); + } +} 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 0000000..5d81160 --- /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 0000000..65217d5 --- /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 0000000..68f77ab --- /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 INVALID_PARAMETER = 29; + + 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 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; + + public static final int USER_NOT_EXIST = 3001; + + public static final int POLICY_NOT_EXIST = 3002; +} 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 new file mode 100644 index 0000000..25ebbaa --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RocketMQSerializable.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.remoting.protocol; + +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 = 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 + byte[] remarkBytes = null; + int remarkLen = 0; + if (cmd.getRemark() != null && cmd.getRemark().length() > 0) { + remarkBytes = cmd.getRemark().getBytes(CHARSET_UTF8); + remarkLen = remarkBytes.length; + } + + // HashMap extFields + byte[] extFieldsBytes = null; + int extLen = 0; + if (cmd.getExtFields() != null && !cmd.getExtFields().isEmpty()) { + extFieldsBytes = mapSerialize(cmd.getExtFields()); + extLen = extFieldsBytes.length; + } + + int totalLen = calTotalLen(remarkLen, extLen); + + ByteBuffer headerBuffer = ByteBuffer.allocate(totalLen); + // int code(~32767) + headerBuffer.putShort((short) cmd.getCode()); + // LanguageCode language + headerBuffer.put(cmd.getLanguage().getCode()); + // int version(~32767) + headerBuffer.putShort((short) cmd.getVersion()); + // int opaque + headerBuffer.putInt(cmd.getOpaque()); + // int flag + headerBuffer.putInt(cmd.getFlag()); + // String remark + if (remarkBytes != null) { + headerBuffer.putInt(remarkBytes.length); + headerBuffer.put(remarkBytes); + } else { + headerBuffer.putInt(0); + } + // HashMap extFields; + if (extFieldsBytes != null) { + headerBuffer.putInt(extFieldsBytes.length); + headerBuffer.put(extFieldsBytes); + } else { + headerBuffer.putInt(0); + } + + return headerBuffer.array(); + } + + public static byte[] mapSerialize(HashMap map) { + // keySize+key+valSize+val + if (null == map || map.isEmpty()) + return null; + + int totalLength = 0; + int kvLength; + Iterator> it = map.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + if (entry.getKey() != null && entry.getValue() != null) { + kvLength = + // keySize + Key + 2 + entry.getKey().getBytes(CHARSET_UTF8).length + // valSize + val + + 4 + entry.getValue().getBytes(CHARSET_UTF8).length; + totalLength += kvLength; + } + } + + ByteBuffer content = ByteBuffer.allocate(totalLength); + byte[] key; + byte[] val; + it = map.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + if (entry.getKey() != null && entry.getValue() != null) { + key = entry.getKey().getBytes(CHARSET_UTF8); + val = entry.getValue().getBytes(CHARSET_UTF8); + + content.putShort((short) key.length); + content.put(key); + + content.putInt(val.length); + content.put(val); + } + } + + return content.array(); + } + + private static int calTotalLen(int remark, int ext) { + // int code(~32767) + int length = 2 + // LanguageCode language + + 1 + // int version(~32767) + + 2 + // int opaque + + 4 + // int flag + + 4 + // String remark + + 4 + remark + // HashMap extFields + + 4 + ext; + + return length; + } + + public static RemotingCommand rocketMQProtocolDecode(final ByteBuf headerBuffer, + int headerLen) throws RemotingCommandException { + RemotingCommand cmd = new RemotingCommand(); + // int code(~32767) + cmd.setCode(headerBuffer.readShort()); + // LanguageCode language + cmd.setLanguage(LanguageCode.valueOf(headerBuffer.readByte())); + // int version(~32767) + cmd.setVersion(headerBuffer.readShort()); + // int opaque + cmd.setOpaque(headerBuffer.readInt()); + // int flag + cmd.setFlag(headerBuffer.readInt()); + // String remark + cmd.setRemark(readStr(headerBuffer, false, headerLen)); + + // HashMap extFields + int extFieldsLength = headerBuffer.readInt(); + if (extFieldsLength > 0) { + 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(ByteBuf byteBuffer, int len) throws RemotingCommandException { + + HashMap map = new HashMap<>(128); + int endIndex = byteBuffer.readerIndex() + len; + + while (byteBuffer.readerIndex() < endIndex) { + String k = readStr(byteBuffer, true, len); + String v = readStr(byteBuffer, false, len); + map.put(k, v); + } + return map; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/SerializeType.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/SerializeType.java new file mode 100644 index 0000000..b040f8f --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/SerializeType.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; + +public enum SerializeType { + JSON((byte) 0), + ROCKETMQ((byte) 1); + + private byte code; + + SerializeType(byte code) { + this.code = code; + } + + public static SerializeType valueOf(byte code) { + for (SerializeType serializeType : SerializeType.values()) { + if (serializeType.getCode() == code) { + return serializeType; + } + } + return null; + } + + public byte getCode() { + return code; + } +} 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 0000000..1ddbfe9 --- /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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/OffsetWrapper.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/OffsetWrapper.java new file mode 100644 index 0000000..a615361 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/OffsetWrapper.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.admin; + +public class OffsetWrapper { + private long brokerOffset; + private long consumerOffset; + private long pullOffset; + private long lastTimestamp; + + public long getBrokerOffset() { + return brokerOffset; + } + + public void setBrokerOffset(long brokerOffset) { + this.brokerOffset = brokerOffset; + } + + public long getConsumerOffset() { + return consumerOffset; + } + + 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; + } + + public void setLastTimestamp(long lastTimestamp) { + this.lastTimestamp = lastTimestamp; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/RollbackStats.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/RollbackStats.java new file mode 100644 index 0000000..4675207 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/RollbackStats.java @@ -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. + */ + +package org.apache.rocketmq.remoting.protocol.admin; + +public class RollbackStats { + private String brokerName; + private long queueId; + private long brokerOffset; + private long consumerOffset; + private long timestampOffset; + private long rollbackOffset; + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public long getQueueId() { + return queueId; + } + + public void setQueueId(long queueId) { + this.queueId = queueId; + } + + public long getBrokerOffset() { + return brokerOffset; + } + + public void setBrokerOffset(long brokerOffset) { + this.brokerOffset = brokerOffset; + } + + public long getConsumerOffset() { + return consumerOffset; + } + + public void setConsumerOffset(long consumerOffset) { + this.consumerOffset = consumerOffset; + } + + public long getTimestampOffset() { + return timestampOffset; + } + + public void setTimestampOffset(long timestampOffset) { + this.timestampOffset = timestampOffset; + } + + public long getRollbackOffset() { + return rollbackOffset; + } + + public void setRollbackOffset(long rollbackOffset) { + this.rollbackOffset = rollbackOffset; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/TopicOffset.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/TopicOffset.java new file mode 100644 index 0000000..be7eeeb --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/TopicOffset.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.admin; + +public class TopicOffset { + private long minOffset; + private long maxOffset; + private long lastUpdateTimestamp; + + 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 getLastUpdateTimestamp() { + return lastUpdateTimestamp; + } + + public void setLastUpdateTimestamp(long lastUpdateTimestamp) { + this.lastUpdateTimestamp = lastUpdateTimestamp; + } + + @Override + public String toString() { + return "TopicOffset{" + + "minOffset=" + minOffset + + ", maxOffset=" + maxOffset + + ", lastUpdateTimestamp=" + lastUpdateTimestamp + + '}'; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/TopicStatsTable.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/TopicStatsTable.java new file mode 100644 index 0000000..5cb2af6 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/TopicStatsTable.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.admin; + +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 double topicPutTps; + + private Map offsetTable = new ConcurrentHashMap<>(); + + public Map getOffsetTable() { + return offsetTable; + } + + public void setOffsetTable(Map offsetTable) { + this.offsetTable = offsetTable; + } + + public double getTopicPutTps() { + return topicPutTps; + } + + public void setTopicPutTps(double topicPutTps) { + this.topicPutTps = topicPutTps; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/AclInfo.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/AclInfo.java new file mode 100644 index 0000000..4607a30 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/AclInfo.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.remoting.protocol.body; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class AclInfo { + + private String subject; + + private List policies; + + public static AclInfo of(String subject, List resources, List actions, + List sourceIps, + String decision) { + AclInfo aclInfo = new AclInfo(); + aclInfo.setSubject(subject); + PolicyInfo policyInfo = PolicyInfo.of(resources, actions, sourceIps, decision); + aclInfo.setPolicies(Collections.singletonList(policyInfo)); + return aclInfo; + } + + public static class PolicyInfo { + + private String policyType; + + private List entries; + + public static PolicyInfo of(List resources, List actions, + List sourceIps, String decision) { + PolicyInfo policyInfo = new PolicyInfo(); + List entries = resources.stream() + .map(resource -> PolicyEntryInfo.of(resource, actions, sourceIps, decision)) + .collect(Collectors.toList()); + policyInfo.setEntries(entries); + return policyInfo; + } + + public String getPolicyType() { + return policyType; + } + + public void setPolicyType(String policyType) { + this.policyType = policyType; + } + + public List getEntries() { + return entries; + } + + public void setEntries(List entries) { + this.entries = entries; + } + } + + public static class PolicyEntryInfo { + private String resource; + + private List actions; + + private List sourceIps; + + private String decision; + + public static PolicyEntryInfo of(String resource, List actions, List sourceIps, + String decision) { + PolicyEntryInfo policyEntryInfo = new PolicyEntryInfo(); + policyEntryInfo.setResource(resource); + policyEntryInfo.setActions(actions); + policyEntryInfo.setSourceIps(sourceIps); + policyEntryInfo.setDecision(decision); + return policyEntryInfo; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public List getActions() { + return actions; + } + + public void setActions(List actions) { + this.actions = actions; + } + + public List getSourceIps() { + return sourceIps; + } + + public void setSourceIps(List sourceIps) { + this.sourceIps = sourceIps; + } + + public String getDecision() { + return decision; + } + + public void setDecision(String decision) { + this.decision = decision; + } + } + + public String getSubject() { + return subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } + + public List getPolicies() { + return policies; + } + + public void setPolicies(List policies) { + this.policies = policies; + } +} 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 0000000..82dcd85 --- /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 0000000..f0e1a8c --- /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 0000000..32497fa --- /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 0000000..a296016 --- /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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BrokerStatsData.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BrokerStatsData.java new file mode 100644 index 0000000..f6649aa --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BrokerStatsData.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.body; + +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class BrokerStatsData extends RemotingSerializable { + + private BrokerStatsItem statsMinute; + + private BrokerStatsItem statsHour; + + private BrokerStatsItem statsDay; + + public BrokerStatsItem getStatsMinute() { + return statsMinute; + } + + public void setStatsMinute(BrokerStatsItem statsMinute) { + this.statsMinute = statsMinute; + } + + public BrokerStatsItem getStatsHour() { + return statsHour; + } + + public void setStatsHour(BrokerStatsItem statsHour) { + this.statsHour = statsHour; + } + + public BrokerStatsItem getStatsDay() { + return statsDay; + } + + public void setStatsDay(BrokerStatsItem statsDay) { + this.statsDay = statsDay; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BrokerStatsItem.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BrokerStatsItem.java new file mode 100644 index 0000000..1a339ad --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BrokerStatsItem.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.body; + +public class BrokerStatsItem { + private long sum; + private double tps; + private double avgpt; + + public long getSum() { + return sum; + } + + public void setSum(long sum) { + this.sum = sum; + } + + public double getTps() { + return tps; + } + + public void setTps(double tps) { + this.tps = tps; + } + + public double getAvgpt() { + return avgpt; + } + + public void setAvgpt(double avgpt) { + this.avgpt = avgpt; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/CMResult.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/CMResult.java new file mode 100644 index 0000000..3e25402 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/CMResult.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.protocol.body; + +public enum CMResult { + CR_SUCCESS, + CR_LATER, + CR_ROLLBACK, + CR_COMMIT, + CR_THROW_EXCEPTION, + CR_RETURN_NULL, +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/CheckClientRequestBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/CheckClientRequestBody.java new file mode 100644 index 0000000..bd482d0 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/CheckClientRequestBody.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 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; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public SubscriptionData getSubscriptionData() { + return subscriptionData; + } + + 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/ClusterInfo.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ClusterInfo.java new file mode 100644 index 0000000..2ee7301 --- /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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/Connection.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/Connection.java new file mode 100644 index 0000000..2e80424 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/Connection.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; + +import org.apache.rocketmq.remoting.protocol.LanguageCode; + +public class Connection { + private String clientId; + private String clientAddr; + private LanguageCode language; + private int version; + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getClientAddr() { + return clientAddr; + } + + public void setClientAddr(String clientAddr) { + this.clientAddr = clientAddr; + } + + 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; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeByWho.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeByWho.java new file mode 100644 index 0000000..ad62493 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeByWho.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.HashSet; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class ConsumeByWho extends RemotingSerializable { + private HashSet consumedGroup = new HashSet<>(); + private HashSet notConsumedGroup = new HashSet<>(); + private String topic; + private int queueId; + private long offset; + + public HashSet getConsumedGroup() { + return consumedGroup; + } + + public void setConsumedGroup(HashSet consumedGroup) { + this.consumedGroup = consumedGroup; + } + + public HashSet getNotConsumedGroup() { + return notConsumedGroup; + } + + public void setNotConsumedGroup(HashSet notConsumedGroup) { + this.notConsumedGroup = notConsumedGroup; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public int getQueueId() { + return queueId; + } + + public void setQueueId(int queueId) { + this.queueId = queueId; + } + + public long getOffset() { + return offset; + } + + public void setOffset(long offset) { + this.offset = offset; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeMessageDirectlyResult.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeMessageDirectlyResult.java new file mode 100644 index 0000000..39da734 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeMessageDirectlyResult.java @@ -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. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class ConsumeMessageDirectlyResult extends RemotingSerializable { + private boolean order = false; + private boolean autoCommit = true; + private CMResult consumeResult; + private String remark; + private long spentTimeMills; + + public boolean isOrder() { + return order; + } + + public void setOrder(boolean order) { + this.order = order; + } + + public boolean isAutoCommit() { + return autoCommit; + } + + public void setAutoCommit(boolean autoCommit) { + this.autoCommit = autoCommit; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public CMResult getConsumeResult() { + return consumeResult; + } + + public void setConsumeResult(CMResult consumeResult) { + this.consumeResult = consumeResult; + } + + public long getSpentTimeMills() { + return spentTimeMills; + } + + public void setSpentTimeMills(long spentTimeMills) { + this.spentTimeMills = spentTimeMills; + } + + @Override + public String toString() { + return "ConsumeMessageDirectlyResult [order=" + order + ", autoCommit=" + autoCommit + + ", consumeResult=" + consumeResult + ", remark=" + remark + ", spentTimeMills=" + + spentTimeMills + "]"; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeQueueData.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeQueueData.java new file mode 100644 index 0000000..34ebc9a --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeQueueData.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.body; + +public class ConsumeQueueData { + + private long physicOffset; + private int physicSize; + private long tagsCode; + private String extendDataJson; + private String bitMap; + private boolean eval; + private String msg; + + public long getPhysicOffset() { + return physicOffset; + } + + public void setPhysicOffset(long physicOffset) { + this.physicOffset = physicOffset; + } + + public int getPhysicSize() { + return physicSize; + } + + public void setPhysicSize(int physicSize) { + this.physicSize = physicSize; + } + + public long getTagsCode() { + return tagsCode; + } + + public void setTagsCode(long tagsCode) { + this.tagsCode = tagsCode; + } + + public String getExtendDataJson() { + return extendDataJson; + } + + public void setExtendDataJson(String extendDataJson) { + this.extendDataJson = extendDataJson; + } + + public String getBitMap() { + return bitMap; + } + + public void setBitMap(String bitMap) { + this.bitMap = bitMap; + } + + public boolean isEval() { + return eval; + } + + public void setEval(boolean eval) { + this.eval = eval; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + @Override + public String toString() { + return "ConsumeQueueData{" + + "physicOffset=" + physicOffset + + ", physicSize=" + physicSize + + ", tagsCode=" + tagsCode + + ", extendDataJson='" + extendDataJson + '\'' + + ", bitMap='" + bitMap + '\'' + + ", eval=" + eval + + ", msg='" + msg + '\'' + + '}'; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeStatsList.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeStatsList.java new file mode 100644 index 0000000..11b36c8 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeStatsList.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.body; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +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 String brokerAddr; + private long totalDiff; + private long totalInflightDiff; + + public List>> getConsumeStatsList() { + return consumeStatsList; + } + + public void setConsumeStatsList(List>> consumeStatsList) { + this.consumeStatsList = consumeStatsList; + } + + public String getBrokerAddr() { + return brokerAddr; + } + + public void setBrokerAddr(String brokerAddr) { + this.brokerAddr = brokerAddr; + } + + public long getTotalDiff() { + return totalDiff; + } + + public void setTotalDiff(long totalDiff) { + this.totalDiff = totalDiff; + } + + public long getTotalInflightDiff() { + return totalInflightDiff; + } + + public void setTotalInflightDiff(long totalInflightDiff) { + this.totalInflightDiff = totalInflightDiff; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeStatus.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeStatus.java new file mode 100644 index 0000000..6f4729c --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeStatus.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; + +public class ConsumeStatus { + private double pullRT; + private double pullTPS; + private double consumeRT; + private double consumeOKTPS; + private double consumeFailedTPS; + + private long consumeFailedMsgs; + + public double getPullRT() { + return pullRT; + } + + public void setPullRT(double pullRT) { + this.pullRT = pullRT; + } + + public double getPullTPS() { + return pullTPS; + } + + public void setPullTPS(double pullTPS) { + this.pullTPS = pullTPS; + } + + public double getConsumeRT() { + return consumeRT; + } + + public void setConsumeRT(double consumeRT) { + this.consumeRT = consumeRT; + } + + public double getConsumeOKTPS() { + return consumeOKTPS; + } + + public void setConsumeOKTPS(double consumeOKTPS) { + this.consumeOKTPS = consumeOKTPS; + } + + public double getConsumeFailedTPS() { + return consumeFailedTPS; + } + + public void setConsumeFailedTPS(double consumeFailedTPS) { + this.consumeFailedTPS = consumeFailedTPS; + } + + public long getConsumeFailedMsgs() { + return consumeFailedMsgs; + } + + public void setConsumeFailedMsgs(long consumeFailedMsgs) { + this.consumeFailedMsgs = consumeFailedMsgs; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumerConnection.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumerConnection.java new file mode 100644 index 0000000..4eb5d7d --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumerConnection.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.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.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 ConcurrentMap subscriptionTable = + new ConcurrentHashMap<>(); + private ConsumeType consumeType; + private MessageModel messageModel; + private ConsumeFromWhere consumeFromWhere; + + public int computeMinVersion() { + int minVersion = Integer.MAX_VALUE; + for (Connection c : this.connectionSet) { + if (c.getVersion() < minVersion) { + minVersion = c.getVersion(); + } + } + + return minVersion; + } + + public HashSet getConnectionSet() { + return connectionSet; + } + + public void setConnectionSet(HashSet connectionSet) { + this.connectionSet = connectionSet; + } + + public ConcurrentMap getSubscriptionTable() { + return subscriptionTable; + } + + public void setSubscriptionTable(ConcurrentHashMap subscriptionTable) { + this.subscriptionTable = subscriptionTable; + } + + 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; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumerOffsetSerializeWrapper.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumerOffsetSerializeWrapper.java new file mode 100644 index 0000000..407be46 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumerOffsetSerializeWrapper.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.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); + private DataVersion dataVersion; + + public ConcurrentMap> getOffsetTable() { + return offsetTable; + } + + public void setOffsetTable(ConcurrentMap> offsetTable) { + this.offsetTable = offsetTable; + } + + 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/ConsumerRunningInfo.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumerRunningInfo.java new file mode 100644 index 0000000..542f930 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumerRunningInfo.java @@ -0,0 +1,363 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Iterator; +import java.util.Map.Entry; +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; + +public class ConsumerRunningInfo extends RemotingSerializable { + public static final String PROP_NAMESERVER_ADDR = "PROP_NAMESERVER_ADDR"; + public static final String PROP_THREADPOOL_CORE_SIZE = "PROP_THREADPOOL_CORE_SIZE"; + public static final String PROP_CONSUME_ORDERLY = "PROP_CONSUMEORDERLY"; + public static final String PROP_CONSUME_TYPE = "PROP_CONSUME_TYPE"; + public static final String PROP_CLIENT_VERSION = "PROP_CLIENT_VERSION"; + public static final String PROP_CONSUMER_START_TIMESTAMP = "PROP_CONSUMER_START_TIMESTAMP"; + + private Properties properties = new Properties(); + + private TreeSet subscriptionSet = new TreeSet<>(); + + private TreeMap mqTable = 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 = isPushType(prev); + + boolean startForAWhile = false; + { + + String property = prev.getProperties().getProperty(ConsumerRunningInfo.PROP_CONSUMER_START_TIMESTAMP); + if (property == null) { + property = String.valueOf(prev.getProperties().get(ConsumerRunningInfo.PROP_CONSUMER_START_TIMESTAMP)); + } + startForAWhile = (System.currentTimeMillis() - Long.parseLong(property)) > (1000 * 60 * 2); + } + + if (push && startForAWhile) { + + { + Iterator> it = criTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + ConsumerRunningInfo current = next.getValue(); + boolean equals = current.getSubscriptionSet().equals(prev.getSubscriptionSet()); + + if (!equals) { + // Different subscription in the same group of consumer + return false; + } + + prev = next.getValue(); + } + + // after consumer.unsubscribe , SubscriptionSet is Empty + //if (prev != null) { + // + // if (prev.getSubscriptionSet().isEmpty()) { + // // Subscription empty! + // return false; + // } + //} + } + } + + return true; + } + + public static boolean isPushType(ConsumerRunningInfo consumerRunningInfo) { + String property = consumerRunningInfo.getProperties().getProperty(ConsumerRunningInfo.PROP_CONSUME_TYPE); + + if (property == null) { + property = ((ConsumeType) consumerRunningInfo.getProperties().get(ConsumerRunningInfo.PROP_CONSUME_TYPE)).name(); + } + return ConsumeType.valueOf(property) == ConsumeType.CONSUME_PASSIVELY; + } + + public static boolean analyzeRebalance(final TreeMap criTable) { + return true; + } + + public static String analyzeProcessQueue(final String clientId, ConsumerRunningInfo info) { + StringBuilder sb = new StringBuilder(); + boolean push = false; + { + String property = info.getProperties().getProperty(ConsumerRunningInfo.PROP_CONSUME_TYPE); + + if (property == null) { + property = ((ConsumeType) info.getProperties().get(ConsumerRunningInfo.PROP_CONSUME_TYPE)).name(); + } + push = ConsumeType.valueOf(property) == ConsumeType.CONSUME_PASSIVELY; + } + + boolean orderMsg = false; + { + String property = info.getProperties().getProperty(ConsumerRunningInfo.PROP_CONSUME_ORDERLY); + orderMsg = Boolean.parseBoolean(property); + } + + if (push) { + Iterator> it = info.getMqTable().entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + MessageQueue mq = next.getKey(); + ProcessQueueInfo pq = next.getValue(); + + if (orderMsg) { + + if (!pq.isLocked()) { + sb.append(String.format("%s %s can't lock for a while, %dms%n", + clientId, + mq, + System.currentTimeMillis() - pq.getLastLockTimestamp())); + } else { + if (pq.isDroped() && pq.getTryUnlockTimes() > 0) { + sb.append(String.format("%s %s unlock %d times, still failed%n", + clientId, + mq, + pq.getTryUnlockTimes())); + } + } + + } else { + long diff = System.currentTimeMillis() - pq.getLastConsumeTimestamp(); + + if (diff > (1000 * 60) && pq.getCachedMsgCount() > 0) { + sb.append(String.format("%s %s can't consume for a while, maybe blocked, %dms%n", + clientId, + mq, + diff)); + } + } + } + } + + return sb.toString(); + } + + public Properties getProperties() { + return properties; + } + + public void setProperties(Properties properties) { + this.properties = properties; + } + + public TreeSet getSubscriptionSet() { + return subscriptionSet; + } + + public void setSubscriptionSet(TreeSet subscriptionSet) { + this.subscriptionSet = subscriptionSet; + } + + public TreeMap getMqTable() { + return mqTable; + } + + public void setMqTable(TreeMap mqTable) { + this.mqTable = mqTable; + } + + public TreeMap getStatusTable() { + return statusTable; + } + + public void setStatusTable(TreeMap statusTable) { + this.statusTable = statusTable; + } + + public TreeMap getUserConsumerInfo() { + return userConsumerInfo; + } + + public String formatString() { + StringBuilder sb = new StringBuilder(); + + { + sb.append("#Consumer Properties#\n"); + Iterator> it = this.properties.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + String item = String.format("%-40s: %s%n", next.getKey().toString(), next.getValue().toString()); + sb.append(item); + } + } + + { + sb.append("\n\n#Consumer Subscription#\n"); + + Iterator it = this.subscriptionSet.iterator(); + int i = 0; + while (it.hasNext()) { + SubscriptionData next = it.next(); + String item = String.format("%03d Topic: %-40s ClassFilter: %-8s SubExpression: %s%n", + ++i, + next.getTopic(), + next.isClassFilterMode(), + next.getSubString()); + + sb.append(item); + } + } + + { + sb.append("\n\n#Consumer Offset#\n"); + sb.append(String.format("%-64s %-32s %-4s %-20s%n", + "#Topic", + "#Broker Name", + "#QID", + "#Consumer Offset" + )); + + Iterator> it = this.mqTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + String item = String.format("%-32s %-32s %-4d %-20d%n", + next.getKey().getTopic(), + next.getKey().getBrokerName(), + next.getKey().getQueueId(), + next.getValue().getCommitOffset()); + + sb.append(item); + } + } + + { + sb.append("\n\n#Consumer MQ Detail#\n"); + sb.append(String.format("%-64s %-32s %-4s %-20s%n", + "#Topic", + "#Broker Name", + "#QID", + "#ProcessQueueInfo" + )); + + 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(), + next.getKey().getQueueId(), + next.getValue().toString()); + + sb.append(item); + } + } + + { + sb.append("\n\n#Consumer RT&TPS#\n"); + sb.append(String.format("%-64s %14s %14s %14s %14s %18s %25s%n", + "#Topic", + "#Pull RT", + "#Pull TPS", + "#Consume RT", + "#ConsumeOK TPS", + "#ConsumeFailed TPS", + "#ConsumeFailedMsgsInHour" + )); + + Iterator> it = this.statusTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + String item = String.format("%-32s %14.2f %14.2f %14.2f %14.2f %18.2f %25d%n", + next.getKey(), + next.getValue().getPullRT(), + next.getValue().getPullTPS(), + next.getValue().getConsumeRT(), + next.getValue().getConsumeOKTPS(), + next.getValue().getConsumeFailedTPS(), + next.getValue().getConsumeFailedMsgs() + ); + + sb.append(item); + } + } + + 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); + } + + return sb.toString(); + } + + public String getJstack() { + return jstack; + } + + 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/CreateTopicListRequestBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/CreateTopicListRequestBody.java new file mode 100644 index 0000000..a72be31 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/CreateTopicListRequestBody.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.List; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class CreateTopicListRequestBody extends RemotingSerializable { + @CFNotNull + private List topicConfigList; + + public CreateTopicListRequestBody() {} + + public CreateTopicListRequestBody(List topicConfigList) { + this.topicConfigList = topicConfigList; + } + + public List getTopicConfigList() { + return topicConfigList; + } + + public void setTopicConfigList(List topicConfigList) { + this.topicConfigList = topicConfigList; + } + +} 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 0000000..8aef636 --- /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 0000000..642331c --- /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 0000000..f338402 --- /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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/GetConsumerStatusBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/GetConsumerStatusBody.java new file mode 100644 index 0000000..f69193a --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/GetConsumerStatusBody.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.body; + +import java.util.HashMap; +import java.util.Map; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +@Deprecated +public class GetConsumerStatusBody extends RemotingSerializable { + private Map messageQueueTable = new HashMap<>(); + private Map> consumerTable = + new HashMap<>(); + + public Map getMessageQueueTable() { + return messageQueueTable; + } + + public void setMessageQueueTable(Map messageQueueTable) { + this.messageQueueTable = messageQueueTable; + } + + public Map> getConsumerTable() { + return consumerTable; + } + + public void setConsumerTable(Map> consumerTable) { + this.consumerTable = consumerTable; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/GroupList.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/GroupList.java new file mode 100644 index 0000000..f2fa43f --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/GroupList.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.protocol.body; + +import java.util.HashSet; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class GroupList extends RemotingSerializable { + private HashSet groupList = new HashSet<>(); + + public HashSet getGroupList() { + return groupList; + } + + public void setGroupList(HashSet groupList) { + this.groupList = 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 0000000..d0f3fb2 --- /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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/KVTable.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/KVTable.java new file mode 100644 index 0000000..73452b4 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/KVTable.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.protocol.body; + +import java.util.HashMap; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class KVTable extends RemotingSerializable { + private HashMap table = new HashMap<>(); + + public HashMap getTable() { + return table; + } + + public void setTable(HashMap table) { + this.table = 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 0000000..6766564 --- /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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/LockBatchResponseBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/LockBatchResponseBody.java new file mode 100644 index 0000000..a46a8aa --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/LockBatchResponseBody.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.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 LockBatchResponseBody extends RemotingSerializable { + + private Set lockOKMQSet = new HashSet<>(); + + public Set getLockOKMQSet() { + return lockOKMQSet; + } + + public void setLockOKMQSet(Set lockOKMQSet) { + this.lockOKMQSet = 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 0000000..fcc0e6f --- /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 0000000..3a6bc3f --- /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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ProcessQueueInfo.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ProcessQueueInfo.java new file mode 100644 index 0000000..075b56e --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ProcessQueueInfo.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.remoting.protocol.body; + +import org.apache.rocketmq.common.UtilAll; + +public class ProcessQueueInfo { + private long commitOffset; + + private long cachedMsgMinOffset; + private long cachedMsgMaxOffset; + private int cachedMsgCount; + private int cachedMsgSizeInMiB; + + private long transactionMsgMinOffset; + private long transactionMsgMaxOffset; + private int transactionMsgCount; + + private boolean locked; + private long tryUnlockTimes; + private long lastLockTimestamp; + + private boolean droped; + private long lastPullTimestamp; + private long lastConsumeTimestamp; + + public long getCommitOffset() { + return commitOffset; + } + + public void setCommitOffset(long commitOffset) { + this.commitOffset = commitOffset; + } + + public long getCachedMsgMinOffset() { + return cachedMsgMinOffset; + } + + public void setCachedMsgMinOffset(long cachedMsgMinOffset) { + this.cachedMsgMinOffset = cachedMsgMinOffset; + } + + public long getCachedMsgMaxOffset() { + return cachedMsgMaxOffset; + } + + public void setCachedMsgMaxOffset(long cachedMsgMaxOffset) { + this.cachedMsgMaxOffset = cachedMsgMaxOffset; + } + + public int getCachedMsgCount() { + return cachedMsgCount; + } + + public void setCachedMsgCount(int cachedMsgCount) { + this.cachedMsgCount = cachedMsgCount; + } + + public long getTransactionMsgMinOffset() { + return transactionMsgMinOffset; + } + + public void setTransactionMsgMinOffset(long transactionMsgMinOffset) { + this.transactionMsgMinOffset = transactionMsgMinOffset; + } + + public long getTransactionMsgMaxOffset() { + return transactionMsgMaxOffset; + } + + public void setTransactionMsgMaxOffset(long transactionMsgMaxOffset) { + this.transactionMsgMaxOffset = transactionMsgMaxOffset; + } + + public int getTransactionMsgCount() { + return transactionMsgCount; + } + + public void setTransactionMsgCount(int transactionMsgCount) { + this.transactionMsgCount = transactionMsgCount; + } + + public boolean isLocked() { + return locked; + } + + public void setLocked(boolean locked) { + this.locked = locked; + } + + public long getTryUnlockTimes() { + return tryUnlockTimes; + } + + public void setTryUnlockTimes(long tryUnlockTimes) { + this.tryUnlockTimes = tryUnlockTimes; + } + + public long getLastLockTimestamp() { + return lastLockTimestamp; + } + + public void setLastLockTimestamp(long lastLockTimestamp) { + this.lastLockTimestamp = lastLockTimestamp; + } + + public boolean isDroped() { + return droped; + } + + public void setDroped(boolean droped) { + this.droped = droped; + } + + public long getLastPullTimestamp() { + return lastPullTimestamp; + } + + public void setLastPullTimestamp(long lastPullTimestamp) { + this.lastPullTimestamp = lastPullTimestamp; + } + + public long getLastConsumeTimestamp() { + return lastConsumeTimestamp; + } + + public void setLastConsumeTimestamp(long lastConsumeTimestamp) { + this.lastConsumeTimestamp = lastConsumeTimestamp; + } + + public int getCachedMsgSizeInMiB() { + return cachedMsgSizeInMiB; + } + + public void setCachedMsgSizeInMiB(final int cachedMsgSizeInMiB) { + this.cachedMsgSizeInMiB = cachedMsgSizeInMiB; + } + + @Override + public String toString() { + return "ProcessQueueInfo [commitOffset=" + commitOffset + ", cachedMsgMinOffset=" + + cachedMsgMinOffset + ", cachedMsgMaxOffset=" + cachedMsgMaxOffset + + ", cachedMsgCount=" + cachedMsgCount + ", cachedMsgSizeInMiB=" + cachedMsgSizeInMiB + + ", transactionMsgMinOffset=" + transactionMsgMinOffset + + ", transactionMsgMaxOffset=" + transactionMsgMaxOffset + ", transactionMsgCount=" + + transactionMsgCount + ", locked=" + locked + ", tryUnlockTimes=" + tryUnlockTimes + + ", lastLockTimestamp=" + UtilAll.timeMillisToHumanString(lastLockTimestamp) + ", droped=" + + droped + ", lastPullTimestamp=" + UtilAll.timeMillisToHumanString(lastPullTimestamp) + + ", lastConsumeTimestamp=" + UtilAll.timeMillisToHumanString(lastConsumeTimestamp) + "]"; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ProducerConnection.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ProducerConnection.java new file mode 100644 index 0000000..91efe5a --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ProducerConnection.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 java.util.HashSet; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class ProducerConnection extends RemotingSerializable { + private HashSet connectionSet = new HashSet<>(); + + public HashSet getConnectionSet() { + return connectionSet; + } + + public void setConnectionSet(HashSet connectionSet) { + this.connectionSet = 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 0000000..bb6d3c8 --- /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 0000000..d4a1d0b --- /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 0000000..fc83b51 --- /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 0000000..8d9b532 --- /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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryConsumeQueueResponseBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryConsumeQueueResponseBody.java new file mode 100644 index 0000000..ecc84c6 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryConsumeQueueResponseBody.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 java.util.List; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; + +public class QueryConsumeQueueResponseBody extends RemotingSerializable { + + private SubscriptionData subscriptionData; + private String filterData; + private List queueData; + private long maxQueueIndex; + private long minQueueIndex; + + public SubscriptionData getSubscriptionData() { + return subscriptionData; + } + + public void setSubscriptionData(SubscriptionData subscriptionData) { + this.subscriptionData = subscriptionData; + } + + public String getFilterData() { + return filterData; + } + + public void setFilterData(String filterData) { + this.filterData = filterData; + } + + public List getQueueData() { + return queueData; + } + + public void setQueueData(List queueData) { + this.queueData = queueData; + } + + public long getMaxQueueIndex() { + return maxQueueIndex; + } + + public void setMaxQueueIndex(long maxQueueIndex) { + this.maxQueueIndex = maxQueueIndex; + } + + public long getMinQueueIndex() { + return minQueueIndex; + } + + public void setMinQueueIndex(long minQueueIndex) { + this.minQueueIndex = minQueueIndex; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryConsumeTimeSpanBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryConsumeTimeSpanBody.java new file mode 100644 index 0000000..599ccc8 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryConsumeTimeSpanBody.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.ArrayList; +import java.util.List; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class QueryConsumeTimeSpanBody extends RemotingSerializable { + List consumeTimeSpanSet = new ArrayList<>(); + + public List getConsumeTimeSpanSet() { + return consumeTimeSpanSet; + } + + public void setConsumeTimeSpanSet(List consumeTimeSpanSet) { + this.consumeTimeSpanSet = consumeTimeSpanSet; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryCorrectionOffsetBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryCorrectionOffsetBody.java new file mode 100644 index 0000000..85be8bc --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryCorrectionOffsetBody.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 java.util.HashMap; +import java.util.Map; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class QueryCorrectionOffsetBody extends RemotingSerializable { + private Map correctionOffsets = new HashMap<>(); + + public Map getCorrectionOffsets() { + return correctionOffsets; + } + + public void setCorrectionOffsets(Map correctionOffsets) { + this.correctionOffsets = 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 0000000..e094a07 --- /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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueueTimeSpan.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueueTimeSpan.java new file mode 100644 index 0000000..6bcb2a3 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueueTimeSpan.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.body; + +import java.util.Date; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageQueue; + +public class QueueTimeSpan { + private MessageQueue messageQueue; + private long minTimeStamp; + private long maxTimeStamp; + private long consumeTimeStamp; + private long delayTime; + + public MessageQueue getMessageQueue() { + return messageQueue; + } + + public void setMessageQueue(MessageQueue messageQueue) { + this.messageQueue = messageQueue; + } + + public long getMinTimeStamp() { + return minTimeStamp; + } + + public void setMinTimeStamp(long minTimeStamp) { + this.minTimeStamp = minTimeStamp; + } + + public long getMaxTimeStamp() { + return maxTimeStamp; + } + + public void setMaxTimeStamp(long maxTimeStamp) { + this.maxTimeStamp = maxTimeStamp; + } + + public long getConsumeTimeStamp() { + return consumeTimeStamp; + } + + public void setConsumeTimeStamp(long consumeTimeStamp) { + this.consumeTimeStamp = consumeTimeStamp; + } + + public String getMinTimeStampStr() { + return UtilAll.formatDate(new Date(minTimeStamp), UtilAll.YYYY_MM_DD_HH_MM_SS_SSS); + } + + public String getMaxTimeStampStr() { + return UtilAll.formatDate(new Date(maxTimeStamp), UtilAll.YYYY_MM_DD_HH_MM_SS_SSS); + } + + public String getConsumeTimeStampStr() { + return UtilAll.formatDate(new Date(consumeTimeStamp), UtilAll.YYYY_MM_DD_HH_MM_SS_SSS); + } + + public long getDelayTime() { + return delayTime; + } + + public void setDelayTime(long delayTime) { + this.delayTime = delayTime; + } +} 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 0000000..99557b1 --- /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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ResetOffsetBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ResetOffsetBody.java new file mode 100644 index 0000000..840bfbf --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ResetOffsetBody.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.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; + } + + public void setOffsetTable(Map offsetTable) { + this.offsetTable = offsetTable; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ResetOffsetBodyForC.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ResetOffsetBodyForC.java new file mode 100644 index 0000000..2470232 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ResetOffsetBodyForC.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.protocol.body; + +import java.util.List; +import org.apache.rocketmq.common.message.MessageQueueForC; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class ResetOffsetBodyForC extends RemotingSerializable { + + private List offsetTable; + + public List getOffsetTable() { + return offsetTable; + } + + public void setOffsetTable(List offsetTable) { + this.offsetTable = offsetTable; + } +} 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 0000000..ab25df0 --- /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 0000000..31aecd0 --- /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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/SubscriptionGroupList.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/SubscriptionGroupList.java new file mode 100644 index 0000000..c343ce2 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/SubscriptionGroupList.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.List; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; + +public class SubscriptionGroupList extends RemotingSerializable { + @CFNotNull + private List groupConfigList; + + public SubscriptionGroupList() {} + + public SubscriptionGroupList(List groupConfigList) { + this.groupConfigList = groupConfigList; + } + + public List getGroupConfigList() { + return groupConfigList; + } + + public void setGroupConfigList(List groupConfigList) { + this.groupConfigList = groupConfigList; + } + +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/SubscriptionGroupWrapper.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/SubscriptionGroupWrapper.java new file mode 100644 index 0000000..7c15902 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/SubscriptionGroupWrapper.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.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; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; + +public class SubscriptionGroupWrapper extends RemotingSerializable { + private ConcurrentMap subscriptionGroupTable = + new ConcurrentHashMap<>(1024); + private DataVersion dataVersion = new DataVersion(); + + public ConcurrentMap getSubscriptionGroupTable() { + return subscriptionGroupTable; + } + + public void setSubscriptionGroupTable( + ConcurrentMap subscriptionGroupTable) { + this.subscriptionGroupTable = subscriptionGroupTable; + } + + 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/SyncStateSet.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/SyncStateSet.java new file mode 100644 index 0000000..f0a71f8 --- /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 0000000..ae9a193 --- /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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/TopicConfigSerializeWrapper.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/TopicConfigSerializeWrapper.java new file mode 100644 index 0000000..b42a5b9 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/TopicConfigSerializeWrapper.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.body; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +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<>(); + private DataVersion dataVersion = new DataVersion(); + + public ConcurrentMap getTopicConfigTable() { + return topicConfigTable; + } + + public void setTopicConfigTable(ConcurrentMap topicConfigTable) { + this.topicConfigTable = topicConfigTable; + } + + 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/TopicList.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/TopicList.java new file mode 100644 index 0000000..0de0bae --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/TopicList.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.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class TopicList extends RemotingSerializable { + private Set topicList = ConcurrentHashMap.newKeySet(); + private String brokerAddr; + + public Set getTopicList() { + return topicList; + } + + public void setTopicList(Set topicList) { + this.topicList = topicList; + } + + public String getBrokerAddr() { + return brokerAddr; + } + + public void setBrokerAddr(String brokerAddr) { + this.brokerAddr = brokerAddr; + } +} 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 0000000..17e16b8 --- /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 0000000..2ad9067 --- /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/body/UserInfo.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/UserInfo.java new file mode 100644 index 0000000..fdcabbf --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/UserInfo.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.body; + +public class UserInfo { + + private String username; + + private String password; + + private String userType; + + private String userStatus; + + public static UserInfo of(String username, String password, String userType) { + UserInfo userInfo = new UserInfo(); + userInfo.setUsername(username); + userInfo.setPassword(password); + userInfo.setUserType(userType); + return userInfo; + } + + public static UserInfo of(String username, String password, String userType, String userStatus) { + UserInfo userInfo = new UserInfo(); + userInfo.setUsername(username); + userInfo.setPassword(password); + userInfo.setUserType(userType); + userInfo.setUserStatus(userStatus); + return userInfo; + } + + 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; + } + + public String getUserType() { + return userType; + } + + public void setUserType(String userType) { + this.userType = userType; + } + + public String getUserStatus() { + return userStatus; + } + + public void setUserStatus(String userStatus) { + this.userStatus = userStatus; + } +} 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 0000000..f291bfc --- /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 0000000..28313fa --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/AckMessageRequestHeader.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.header; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +@RocketMQAction(value = RequestCode.ACK_MESSAGE, action = Action.SUB) +public class AckMessageRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String consumerGroup; + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + 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 0000000..6feae1d --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/AddBrokerRequestHeader.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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.ADD_BROKER, resource = ResourceType.CLUSTER, action = Action.UPDATE) +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 0000000..ebd32cc --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ChangeInvisibleTimeRequestHeader.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.remoting.protocol.header; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +@RocketMQAction(value = RequestCode.CHANGE_MESSAGE_INVISIBLETIME, action = Action.SUB) +public class ChangeInvisibleTimeRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String consumerGroup; + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + 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 0000000..c3b1cca --- /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/CheckRocksdbCqWriteProgressRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CheckRocksdbCqWriteProgressRequestHeader.java new file mode 100644 index 0000000..f679077 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CheckRocksdbCqWriteProgressRequestHeader.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.remoting.protocol.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +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.RequestCode; + +@RocketMQAction(value = RequestCode.CHECK_ROCKSDB_CQ_WRITE_PROGRESS, action = Action.GET) +public class CheckRocksdbCqWriteProgressRequestHeader implements CommandCustomHeader { + + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + private String topic; + + private long checkStoreTime; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public long getCheckStoreTime() { + return checkStoreTime; + } + + public void setCheckStoreTime(long checkStoreTime) { + this.checkStoreTime = checkStoreTime; + } + +} 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 0000000..7251488 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CheckTransactionStateRequestHeader.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. + */ + +/** + * $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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; + +@RocketMQAction(value = RequestCode.CHECK_TRANSACTION_STATE, action = Action.PUB) +public class CheckTransactionStateRequestHeader extends RpcRequestHeader { + @RocketMQResource(ResourceType.TOPIC) + private String topic; + @CFNotNull + private Long tranStateTableOffset; + @CFNotNull + private Long commitLogOffset; + private String msgId; + private String transactionId; + private String offsetMsgId; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + 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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CheckTransactionStateResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CheckTransactionStateResponseHeader.java new file mode 100644 index 0000000..9aa2d7a --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CheckTransactionStateResponseHeader.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: EndTransactionResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +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.exception.RemotingCommandException; + +public class CheckTransactionStateResponseHeader implements CommandCustomHeader { + @CFNotNull + private String producerGroup; + @CFNotNull + private Long tranStateTableOffset; + @CFNotNull + private Long commitLogOffset; + @CFNotNull + private Integer commitOrRollback; // TRANSACTION_COMMIT_TYPE + + // TRANSACTION_ROLLBACK_TYPE + + @Override + public void checkFields() throws RemotingCommandException { + if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == this.commitOrRollback) { + return; + } + + if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE == this.commitOrRollback) { + return; + } + + throw new RemotingCommandException("commitOrRollback field wrong"); + } + + public String getProducerGroup() { + return producerGroup; + } + + public void setProducerGroup(String producerGroup) { + this.producerGroup = producerGroup; + } + + 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 Integer getCommitOrRollback() { + return commitOrRollback; + } + + public void setCommitOrRollback(Integer commitOrRollback) { + this.commitOrRollback = commitOrRollback; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CloneGroupOffsetRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CloneGroupOffsetRequestHeader.java new file mode 100644 index 0000000..7475d26 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CloneGroupOffsetRequestHeader.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: DeleteTopicRequestHeader.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.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; + +@RocketMQAction(value = RequestCode.CLONE_GROUP_OFFSET, action = Action.UPDATE) +public class CloneGroupOffsetRequestHeader extends RpcRequestHeader { + @CFNotNull + private String srcGroup; + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String destGroup; + @RocketMQResource(ResourceType.TOPIC) + private String topic; + private boolean offline; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getDestGroup() { + return destGroup; + } + + public void setDestGroup(String destGroup) { + this.destGroup = destGroup; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getSrcGroup() { + + return srcGroup; + } + + public void setSrcGroup(String srcGroup) { + this.srcGroup = srcGroup; + } + + public boolean isOffline() { + return offline; + } + + 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 0000000..d160c1e --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ConsumeMessageDirectlyResultRequestHeader.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.remoting.protocol.header; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +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.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; + +@RocketMQAction(value = RequestCode.CONSUME_MESSAGE_DIRECTLY, action = Action.SUB) +public class ConsumeMessageDirectlyResultRequestHeader extends TopicRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String consumerGroup; + @CFNullable + private String clientId; + @CFNullable + private String msgId; + @CFNullable + private String brokerName; + @CFNullable + @RocketMQResource(ResourceType.TOPIC) + 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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ConsumerSendMsgBackRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ConsumerSendMsgBackRequestHeader.java new file mode 100644 index 0000000..078aba8 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ConsumerSendMsgBackRequestHeader.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.remoting.protocol.header; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +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.RequestCode; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; + +@RocketMQAction(value = RequestCode.CONSUMER_SEND_MSG_BACK, action = Action.SUB) +public class ConsumerSendMsgBackRequestHeader extends RpcRequestHeader { + @CFNotNull + private Long offset; + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String group; + @CFNotNull + private Integer delayLevel; + private String originMsgId; + @RocketMQResource(ResourceType.TOPIC) + private String originTopic; + @CFNullable + private boolean unitMode = false; + private Integer maxReconsumeTimes; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public Long getOffset() { + return offset; + } + + public void setOffset(Long offset) { + this.offset = offset; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public Integer getDelayLevel() { + return delayLevel; + } + + public void setDelayLevel(Integer delayLevel) { + this.delayLevel = delayLevel; + } + + public String getOriginMsgId() { + return originMsgId; + } + + public void setOriginMsgId(String originMsgId) { + this.originMsgId = originMsgId; + } + + public String getOriginTopic() { + return originTopic; + } + + public void setOriginTopic(String originTopic) { + this.originTopic = originTopic; + } + + public boolean isUnitMode() { + return unitMode; + } + + public void setUnitMode(boolean unitMode) { + this.unitMode = unitMode; + } + + public Integer getMaxReconsumeTimes() { + return maxReconsumeTimes; + } + + public void setMaxReconsumeTimes(final Integer maxReconsumeTimes) { + this.maxReconsumeTimes = maxReconsumeTimes; + } + + @Override + public String toString() { + 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/CreateAclRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CreateAclRequestHeader.java new file mode 100644 index 0000000..5839d3e --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CreateAclRequestHeader.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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.AUTH_CREATE_ACL, resource = ResourceType.CLUSTER, action = Action.UPDATE) +public class CreateAclRequestHeader implements CommandCustomHeader { + + private String subject; + + public CreateAclRequestHeader() { + } + + public CreateAclRequestHeader(String subject) { + this.subject = subject; + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getSubject() { + return subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CreateTopicListRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CreateTopicListRequestHeader.java new file mode 100644 index 0000000..615de75 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CreateTopicListRequestHeader.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.remoting.protocol.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; + +@RocketMQAction(value = RequestCode.UPDATE_AND_CREATE_TOPIC_LIST, action = Action.CREATE) +public class CreateTopicListRequestHeader extends RpcRequestHeader { + @Override + public void checkFields() throws RemotingCommandException { + + } +} 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 0000000..6a1f1cb --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CreateTopicRequestHeader.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. + */ + +/** + * $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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +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.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; + +@RocketMQAction(value = RequestCode.UPDATE_AND_CREATE_TOPIC, action = Action.CREATE) +public class CreateTopicRequestHeader extends TopicRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + 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/CreateUserRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CreateUserRequestHeader.java new file mode 100644 index 0000000..32e34ed --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CreateUserRequestHeader.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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.AUTH_CREATE_USER, resource = ResourceType.CLUSTER, action = Action.UPDATE) +public class CreateUserRequestHeader implements CommandCustomHeader { + + private String username; + + public CreateUserRequestHeader() { + } + + public CreateUserRequestHeader(String username) { + this.username = username; + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/DeleteAclRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/DeleteAclRequestHeader.java new file mode 100644 index 0000000..a1f06a2 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/DeleteAclRequestHeader.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.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.AUTH_DELETE_ACL, resource = ResourceType.CLUSTER, action = Action.UPDATE) +public class DeleteAclRequestHeader implements CommandCustomHeader { + + private String subject; + + private String policyType; + + private String resource; + + public DeleteAclRequestHeader() { + } + + public DeleteAclRequestHeader(String subject, String resource) { + this.subject = subject; + this.resource = resource; + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getSubject() { + return subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } + + public String getPolicyType() { + return policyType; + } + + public void setPolicyType(String policyType) { + this.policyType = policyType; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/DeleteSubscriptionGroupRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/DeleteSubscriptionGroupRequestHeader.java new file mode 100644 index 0000000..e936963 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/DeleteSubscriptionGroupRequestHeader.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.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; + +@RocketMQAction(value = RequestCode.DELETE_SUBSCRIPTIONGROUP, action = Action.DELETE) +public class DeleteSubscriptionGroupRequestHeader extends RpcRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String groupName; + + private boolean cleanOffset = false; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getGroupName() { + return groupName; + } + + public void setGroupName(String groupName) { + this.groupName = groupName; + } + + public boolean isCleanOffset() { + return cleanOffset; + } + + public void setCleanOffset(boolean cleanOffset) { + this.cleanOffset = cleanOffset; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/DeleteTopicRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/DeleteTopicRequestHeader.java new file mode 100644 index 0000000..ea66ed9 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/DeleteTopicRequestHeader.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: DeleteTopicRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; + +@RocketMQAction(value = RequestCode.DELETE_TOPIC_IN_BROKER, action = Action.DELETE) +public class DeleteTopicRequestHeader extends TopicRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + 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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/DeleteUserRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/DeleteUserRequestHeader.java new file mode 100644 index 0000000..00eb1e8 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/DeleteUserRequestHeader.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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.AUTH_DELETE_USER, resource = ResourceType.CLUSTER, action = Action.UPDATE) +public class DeleteUserRequestHeader implements CommandCustomHeader { + + private String username; + + public DeleteUserRequestHeader() { + } + + public DeleteUserRequestHeader(String username) { + this.username = username; + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/EndTransactionRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/EndTransactionRequestHeader.java new file mode 100644 index 0000000..cef464a --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/EndTransactionRequestHeader.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.remoting.protocol.header; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +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.RequestCode; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; + +@RocketMQAction(value = RequestCode.END_TRANSACTION, action = Action.PUB) +public class EndTransactionRequestHeader extends RpcRequestHeader { + @RocketMQResource(ResourceType.TOPIC) + private String topic; + @CFNotNull + private String producerGroup; + @CFNotNull + private Long tranStateTableOffset; + @CFNotNull + private Long commitLogOffset; + @CFNotNull + private Integer commitOrRollback; // TRANSACTION_COMMIT_TYPE + // TRANSACTION_ROLLBACK_TYPE + // TRANSACTION_NOT_TYPE + + @CFNullable + private Boolean fromTransactionCheck = false; + + @CFNotNull + private String msgId; + + private String transactionId; + + @Override + public void checkFields() throws RemotingCommandException { + if (MessageSysFlag.TRANSACTION_NOT_TYPE == this.commitOrRollback) { + return; + } + + if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == this.commitOrRollback) { + return; + } + + if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE == this.commitOrRollback) { + return; + } + + throw new RemotingCommandException("commitOrRollback field wrong"); + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getProducerGroup() { + return producerGroup; + } + + public void setProducerGroup(String producerGroup) { + this.producerGroup = producerGroup; + } + + 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 Integer getCommitOrRollback() { + return commitOrRollback; + } + + public void setCommitOrRollback(Integer commitOrRollback) { + this.commitOrRollback = commitOrRollback; + } + + public Boolean getFromTransactionCheck() { + return fromTransactionCheck; + } + + public void setFromTransactionCheck(Boolean fromTransactionCheck) { + this.fromTransactionCheck = fromTransactionCheck; + } + + 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; + } + + @Override + public String toString() { + 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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/EndTransactionResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/EndTransactionResponseHeader.java new file mode 100644 index 0000000..f6aa51e --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/EndTransactionResponseHeader.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. + */ + +/** + * $Id: EndTransactionResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class EndTransactionResponseHeader implements CommandCustomHeader { + + @Override + public void checkFields() throws 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 0000000..103b2ac --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ExchangeHAInfoRequestHeader.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; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.EXCHANGE_BROKER_HA_INFO,resource = ResourceType.CLUSTER, action = Action.UPDATE) +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 0000000..3bbbc4c --- /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/ExportRocksDBConfigToJsonRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ExportRocksDBConfigToJsonRequestHeader.java new file mode 100644 index 0000000..8354f83 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ExportRocksDBConfigToJsonRequestHeader.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.protocol.header; + +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +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.RequestCode; + +@RocketMQAction(value = RequestCode.EXPORT_ROCKSDB_CONFIG_TO_JSON, resource = ResourceType.CLUSTER, action = Action.GET) +public class ExportRocksDBConfigToJsonRequestHeader implements CommandCustomHeader { + private static final String CONFIG_TYPE_SEPARATOR = ";"; + + public enum ConfigType { + TOPICS("topics"), + SUBSCRIPTION_GROUPS("subscriptionGroups"), + CONSUMER_OFFSETS("consumerOffsets"); + + private final String typeName; + + ConfigType(String typeName) { + this.typeName = typeName; + } + + public static ConfigType getConfigTypeByName(String typeName) { + for (ConfigType configType : ConfigType.values()) { + if (configType.getTypeName().equalsIgnoreCase(typeName.trim())) { + return configType; + } + } + throw new IllegalArgumentException("Unknown config type: " + typeName); + } + + public static List fromString(String ordinal) { + String[] configTypeNames = StringUtils.split(ordinal, CONFIG_TYPE_SEPARATOR); + List configTypes = new ArrayList<>(); + for (String configTypeName : configTypeNames) { + if (StringUtils.isNotEmpty(configTypeName)) { + configTypes.add(getConfigTypeByName(configTypeName)); + } + } + return configTypes; + } + + public static String toString(List configTypes) { + StringBuilder sb = new StringBuilder(); + for (ConfigType configType : configTypes) { + sb.append(configType.getTypeName()).append(CONFIG_TYPE_SEPARATOR); + } + return sb.toString(); + } + + public String getTypeName() { + return typeName; + } + } + + @CFNotNull + private String configType; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public List fetchConfigType() { + return ConfigType.fromString(configType); + } + + public void updateConfigType(List configType) { + this.configType = ConfigType.toString(configType); + } + + public String getConfigType() { + return configType; + } + + public void setConfigType(String configType) { + this.configType = configType; + } +} \ No newline at end of file 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 0000000..a6a4a77 --- /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/GetAclRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAclRequestHeader.java new file mode 100644 index 0000000..09b9df6 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAclRequestHeader.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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.AUTH_GET_ACL, resource = ResourceType.CLUSTER, action = Action.GET) +public class GetAclRequestHeader implements CommandCustomHeader { + + private String subject; + + public GetAclRequestHeader() { + } + + public GetAclRequestHeader(String subject) { + this.subject = subject; + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getSubject() { + return subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } +} 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 0000000..e57a589 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllProducerInfoRequestHeader.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.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.GET_ALL_PRODUCER_INFO, resource = ResourceType.CLUSTER, action = Action.GET) +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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllTopicConfigResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllTopicConfigResponseHeader.java new file mode 100644 index 0000000..566ce16 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllTopicConfigResponseHeader.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. + */ + +/** + * $Id: GetAllTopicConfigResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.GET_ALL_TOPIC_CONFIG, resource = ResourceType.TOPIC, action = Action.LIST) +public class GetAllTopicConfigResponseHeader implements CommandCustomHeader { + + @Override + public void checkFields() throws RemotingCommandException { + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetBrokerConfigResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetBrokerConfigResponseHeader.java new file mode 100644 index 0000000..bcc6721 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetBrokerConfigResponseHeader.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. + */ + +/** + * $Id: GetBrokerConfigResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +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 GetBrokerConfigResponseHeader implements CommandCustomHeader { + @CFNotNull + private String version; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } +} 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 0000000..d2fd6d4 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetBrokerMemberGroupRequestHeader.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.remoting.protocol.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +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.RequestCode; + +@RocketMQAction(value = RequestCode.GET_BROKER_MEMBER_GROUP, action = Action.GET) +public class GetBrokerMemberGroupRequestHeader implements CommandCustomHeader { + @CFNotNull + @RocketMQResource(ResourceType.CLUSTER) + 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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumeStatsInBrokerHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumeStatsInBrokerHeader.java new file mode 100644 index 0000000..7d47d53 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumeStatsInBrokerHeader.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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +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.RequestCode; + +@RocketMQAction(value = RequestCode.GET_BROKER_CONSUME_STATS, resource = ResourceType.CLUSTER, action = Action.GET) +public class GetConsumeStatsInBrokerHeader implements CommandCustomHeader { + @CFNotNull + private boolean isOrder; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public boolean isOrder() { + return isOrder; + } + + public void setIsOrder(boolean isOrder) { + this.isOrder = isOrder; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumeStatsRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumeStatsRequestHeader.java new file mode 100644 index 0000000..2c51c3f --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumeStatsRequestHeader.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.remoting.protocol.header; + +import com.google.common.base.MoreObjects; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; + +@RocketMQAction(value = RequestCode.GET_CONSUME_STATS, action = Action.GET) +public class GetConsumeStatsRequestHeader extends TopicRequestHeader { + private static final String TOPIC_NAME_SEPARATOR = ";"; + + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String consumerGroup; + + @RocketMQResource(ResourceType.TOPIC) + private String topic; + + // if topicList is provided, topic will be ignored + @RocketMQResource(value = ResourceType.TOPIC, splitter = TOPIC_NAME_SEPARATOR) + private String topicList; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public List fetchTopicList() { + if (StringUtils.isBlank(topicList)) { + return Collections.emptyList(); + } + return Arrays.asList(StringUtils.split(topicList, TOPIC_NAME_SEPARATOR)); + } + + public void updateTopicList(List topicList) { + if (topicList == null || topicList.isEmpty()) { + return; + } + StringBuilder sb = new StringBuilder(); + topicList.forEach(topic -> sb.append(topic).append(TOPIC_NAME_SEPARATOR)); + this.setTopicList(sb.toString()); + } + + public String getTopicList() { + return topicList; + } + + public void setTopicList(String topicList) { + this.topicList = topicList; + } + + 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; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("consumerGroup", consumerGroup) + .add("topic", topic) + .toString(); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerConnectionListRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerConnectionListRequestHeader.java new file mode 100644 index 0000000..64f0e2d --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerConnectionListRequestHeader.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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; + +@RocketMQAction(value = RequestCode.GET_CONSUMER_CONNECTION_LIST, action = Action.GET) +public class GetConsumerConnectionListRequestHeader extends RpcRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String consumerGroup; + + @Override + public void checkFields() throws RemotingCommandException { + // To change body of implemented methods use File | Settings | File + // Templates. + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerListByGroupRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerListByGroupRequestHeader.java new file mode 100644 index 0000000..cd34cbf --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerListByGroupRequestHeader.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.protocol.header; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.GET_CONSUMER_LIST_BY_GROUP, action = Action.SUB) +public class GetConsumerListByGroupRequestHeader extends RpcRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String consumerGroup; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("consumerGroup", consumerGroup) + .toString(); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerListByGroupResponseBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerListByGroupResponseBody.java new file mode 100644 index 0000000..545ea12 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerListByGroupResponseBody.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.header; + +import java.util.List; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class GetConsumerListByGroupResponseBody extends RemotingSerializable { + private List consumerIdList; + + public List getConsumerIdList() { + return consumerIdList; + } + + public void setConsumerIdList(List consumerIdList) { + this.consumerIdList = consumerIdList; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerListByGroupResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerListByGroupResponseHeader.java new file mode 100644 index 0000000..42ca5f1 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerListByGroupResponseHeader.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.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class GetConsumerListByGroupResponseHeader implements CommandCustomHeader { + + @Override + public void checkFields() throws RemotingCommandException { + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerRunningInfoRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerRunningInfoRequestHeader.java new file mode 100644 index 0000000..894e726 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerRunningInfoRequestHeader.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.header; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +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; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.GET_CONSUMER_RUNNING_INFO, action = Action.GET) +public class GetConsumerRunningInfoRequestHeader extends RpcRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String consumerGroup; + @CFNotNull + private String clientId; + @CFNullable + private boolean jstackEnable; + + @Override + public void checkFields() throws RemotingCommandException { + } + + 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 isJstackEnable() { + return jstackEnable; + } + + 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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerStatusRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerStatusRequestHeader.java new file mode 100644 index 0000000..8e2b850 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerStatusRequestHeader.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.header; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +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; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.INVOKE_BROKER_TO_GET_CONSUMER_STATUS, action = Action.GET) +public class GetConsumerStatusRequestHeader extends TopicRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + private String topic; + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String group; + @CFNullable + private String clientAddr; + + @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 String getClientAddr() { + return clientAddr; + } + + 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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetEarliestMsgStoretimeRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetEarliestMsgStoretimeRequestHeader.java new file mode 100644 index 0000000..b8715f4 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetEarliestMsgStoretimeRequestHeader.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. + */ + +/** + * $Id: GetEarliestMsgStoretimeRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +@RocketMQAction(value = RequestCode.GET_EARLIEST_MSG_STORETIME, action = Action.GET) +public class GetEarliestMsgStoretimeRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + private String topic; + @CFNotNull + private Integer queueId; + + @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; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetEarliestMsgStoretimeResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetEarliestMsgStoretimeResponseHeader.java new file mode 100644 index 0000000..9498352 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetEarliestMsgStoretimeResponseHeader.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. + */ + +/** + * $Id: GetEarliestMsgStoretimeResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +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 GetEarliestMsgStoretimeResponseHeader implements CommandCustomHeader { + @CFNotNull + private Long timestamp; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public Long getTimestamp() { + return timestamp; + } + + public void setTimestamp(Long timestamp) { + this.timestamp = timestamp; + } +} 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 0000000..68b36a2 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMaxOffsetRequestHeader.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: 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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +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.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +@RocketMQAction(value = RequestCode.GET_MAX_OFFSET, action = Action.GET) +public class GetMaxOffsetRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + 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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMaxOffsetResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMaxOffsetResponseHeader.java new file mode 100644 index 0000000..1e2f687 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMaxOffsetResponseHeader.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. + */ + +/** + * $Id: GetMaxOffsetResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +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 GetMaxOffsetResponseHeader implements CommandCustomHeader { + @CFNotNull + private Long offset; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public Long getOffset() { + return offset; + } + + public void setOffset(Long offset) { + this.offset = offset; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMinOffsetRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMinOffsetRequestHeader.java new file mode 100644 index 0000000..8515dc3 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMinOffsetRequestHeader.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. + */ + +/** + * $Id: GetMinOffsetRequestHeader.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.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +@RocketMQAction(value = RequestCode.GET_MIN_OFFSET, action = Action.GET) +public class GetMinOffsetRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + private String topic; + @CFNotNull + private Integer queueId; + + @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; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("topic", topic) + .add("queueId", queueId) + .toString(); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMinOffsetResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMinOffsetResponseHeader.java new file mode 100644 index 0000000..b4e1ae9 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMinOffsetResponseHeader.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. + */ + +/** + * $Id: GetMinOffsetResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +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 GetMinOffsetResponseHeader implements CommandCustomHeader { + @CFNotNull + private Long offset; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public Long getOffset() { + return offset; + } + + public void setOffset(Long offset) { + this.offset = offset; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetProducerConnectionListRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetProducerConnectionListRequestHeader.java new file mode 100644 index 0000000..f3f2d50 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetProducerConnectionListRequestHeader.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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; + +@RocketMQAction(value = RequestCode.GET_PRODUCER_CONNECTION_LIST, resource = ResourceType.CLUSTER, action = Action.GET) +public class GetProducerConnectionListRequestHeader extends RpcRequestHeader { + @CFNotNull + private String producerGroup; + + @Override + public void checkFields() throws RemotingCommandException { + // To change body of implemented methods use File | Settings | File + // Templates. + } + + public String getProducerGroup() { + return producerGroup; + } + + public void setProducerGroup(String producerGroup) { + this.producerGroup = 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 0000000..5d8ac7c --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetSubscriptionGroupConfigRequestHeader.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. + */ + +/** + * $Id: GetAllTopicConfigResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; + +@RocketMQAction(value = RequestCode.GET_SUBSCRIPTIONGROUP_CONFIG, action = Action.GET) +public class GetSubscriptionGroupConfigRequestHeader extends RpcRequestHeader { + + @Override + public void checkFields() throws RemotingCommandException { + } + + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + 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 0000000..62f3de2 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetTopicConfigRequestHeader.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.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; + +@RocketMQAction(value = RequestCode.GET_TOPIC_CONFIG, action = Action.GET) +public class GetTopicConfigRequestHeader extends TopicRequestHeader { + @Override + public void checkFields() throws RemotingCommandException { + } + + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + 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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetTopicStatsInfoRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetTopicStatsInfoRequestHeader.java new file mode 100644 index 0000000..7c90995 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetTopicStatsInfoRequestHeader.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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; + +@RocketMQAction(value = RequestCode.GET_TOPIC_STATS_INFO, action = Action.GET) +public class GetTopicStatsInfoRequestHeader extends TopicRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + 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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetTopicsByClusterRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetTopicsByClusterRequestHeader.java new file mode 100644 index 0000000..b2e7898 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetTopicsByClusterRequestHeader.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.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +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.RequestCode; + +@RocketMQAction(value = RequestCode.GET_TOPICS_BY_CLUSTER, resource = ResourceType.TOPIC, action = Action.LIST) +public class GetTopicsByClusterRequestHeader implements CommandCustomHeader { + @CFNotNull + private String cluster; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getCluster() { + return cluster; + } + + public void setCluster(String cluster) { + this.cluster = cluster; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetUserRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetUserRequestHeader.java new file mode 100644 index 0000000..cd95264 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetUserRequestHeader.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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.AUTH_GET_USER, resource = ResourceType.CLUSTER, action = Action.GET) +public class GetUserRequestHeader implements CommandCustomHeader { + + private String username; + + public GetUserRequestHeader() { + } + + public GetUserRequestHeader(String username) { + this.username = username; + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } +} 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 0000000..5a3c0fd --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/HeartbeatRequestHeader.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.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; + +@RocketMQAction(value = RequestCode.HEART_BEAT, resource = ResourceType.GROUP, action = {Action.PUB, Action.SUB}) +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 0000000..ac63b97 --- /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/ListAclsRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ListAclsRequestHeader.java new file mode 100644 index 0000000..c929eef --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ListAclsRequestHeader.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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.AUTH_LIST_ACL, resource = ResourceType.CLUSTER, action = Action.GET) +public class ListAclsRequestHeader implements CommandCustomHeader { + + private String subjectFilter; + + private String resourceFilter; + + public ListAclsRequestHeader() { + } + + public ListAclsRequestHeader(String subjectFilter, String resourceFilter) { + this.subjectFilter = subjectFilter; + this.resourceFilter = resourceFilter; + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getSubjectFilter() { + return subjectFilter; + } + + public void setSubjectFilter(String subjectFilter) { + this.subjectFilter = subjectFilter; + } + + public String getResourceFilter() { + return resourceFilter; + } + + public void setResourceFilter(String resourceFilter) { + this.resourceFilter = resourceFilter; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ListUsersRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ListUsersRequestHeader.java new file mode 100644 index 0000000..3bf9424 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ListUsersRequestHeader.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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.AUTH_LIST_USER, resource = ResourceType.CLUSTER, action = Action.GET) +public class ListUsersRequestHeader implements CommandCustomHeader { + + private String filter; + + public ListUsersRequestHeader() { + } + + public ListUsersRequestHeader(String filter) { + this.filter = filter; + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getFilter() { + return filter; + } + + public void setFilter(String filter) { + this.filter = filter; + } +} 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 0000000..970974c --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/LockBatchMqRequestHeader.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.protocol.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; + +@RocketMQAction(value = RequestCode.LOCK_BATCH_MQ, action = Action.SUB) +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 0000000..0e484f8 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/NotificationRequestHeader.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.remoting.protocol.header; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +@RocketMQAction(value = RequestCode.NOTIFICATION, action = Action.SUB) +public class NotificationRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String consumerGroup; + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + 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 0000000..027717e --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/NotificationResponseHeader.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 NotificationResponseHeader implements CommandCustomHeader { + + + @CFNotNull + private boolean hasMsg = false; + + private boolean pollingFull = false; + + public boolean isHasMsg() { + return hasMsg; + } + + public boolean isPollingFull() { + return pollingFull; + } + + public void setPollingFull(boolean pollingFull) { + this.pollingFull = pollingFull; + } + + 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 0000000..e9a348a --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/NotifyBrokerRoleChangedRequestHeader.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.remoting.protocol.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.NOTIFY_BROKER_ROLE_CHANGED, resource = ResourceType.CLUSTER, action = Action.UPDATE) +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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/NotifyConsumerIdsChangedRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/NotifyConsumerIdsChangedRequestHeader.java new file mode 100644 index 0000000..eb109dc --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/NotifyConsumerIdsChangedRequestHeader.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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.NOTIFY_CONSUMER_IDS_CHANGED, action = Action.SUB) +public class NotifyConsumerIdsChangedRequestHeader extends RpcRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String consumerGroup; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = 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 0000000..8b451e2 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/NotifyMinBrokerIdChangeRequestHeader.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.remoting.protocol.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.NOTIFY_MIN_BROKER_ID_CHANGE, resource = ResourceType.CLUSTER, action = Action.UPDATE) +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 0000000..61746a6 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PeekMessageRequestHeader.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.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +@RocketMQAction(value = RequestCode.PEEK_MESSAGE, action = Action.SUB) +public class PeekMessageRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + private String topic; + @CFNotNull + private int queueId; + @CFNotNull + private int maxMsgNums; + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + 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 0000000..1959938 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PollingInfoRequestHeader.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.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +@RocketMQAction(value = RequestCode.POLLING_INFO, action = Action.GET) +public class PollingInfoRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String consumerGroup; + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + 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 0000000..7d2d852 --- /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 0000000..8a7ab4f --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PopMessageRequestHeader.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.remoting.protocol.header; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +@RocketMQAction(value = RequestCode.POP_MESSAGE, action = Action.SUB) +public class PopMessageRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String consumerGroup; + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + 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 0000000..da17733 --- /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 0000000..5785615 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PullMessageRequestHeader.java @@ -0,0 +1,337 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +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.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +@RocketMQAction(value = RequestCode.PULL_MESSAGE, action = Action.SUB) +public class PullMessageRequestHeader extends TopicQueueRequestHeader implements FastCodesHeader { + + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String consumerGroup; + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + 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 0000000..bc356f2 --- /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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryConsumeQueueRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryConsumeQueueRequestHeader.java new file mode 100644 index 0000000..c436f1b --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryConsumeQueueRequestHeader.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.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.QUERY_CONSUME_QUEUE, action = Action.GET) +public class QueryConsumeQueueRequestHeader extends TopicQueueRequestHeader { + + @RocketMQResource(ResourceType.TOPIC) + private String topic; + private int queueId; + private long index; + private int count; + @RocketMQResource(ResourceType.GROUP) + private String 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 getIndex() { + return index; + } + + public void setIndex(long index) { + this.index = index; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + @Override + public void checkFields() throws RemotingCommandException { + + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryConsumeTimeSpanRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryConsumeTimeSpanRequestHeader.java new file mode 100644 index 0000000..d9f134d --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryConsumeTimeSpanRequestHeader.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.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; + +@RocketMQAction(value = RequestCode.QUERY_CONSUME_TIME_SPAN, action = Action.GET) +public class QueryConsumeTimeSpanRequestHeader extends TopicRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + private String topic; + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String group; + + @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; + } +} 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 0000000..f56aa96 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryConsumerOffsetRequestHeader.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. + */ + +/** + * $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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +@RocketMQAction(value = RequestCode.QUERY_CONSUMER_OFFSET, action = Action.GET) +public class QueryConsumerOffsetRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String consumerGroup; + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + 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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryConsumerOffsetResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryConsumerOffsetResponseHeader.java new file mode 100644 index 0000000..1ee706f --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryConsumerOffsetResponseHeader.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. + */ + +/** + * $Id: QueryConsumerOffsetResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +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 QueryConsumerOffsetResponseHeader implements CommandCustomHeader { + @CFNotNull + private Long offset; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public Long getOffset() { + return offset; + } + + public void setOffset(Long offset) { + this.offset = offset; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryCorrectionOffsetHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryCorrectionOffsetHeader.java new file mode 100644 index 0000000..27a3d35 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryCorrectionOffsetHeader.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. + */ + +/** + * $Id: GetMinOffsetRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.QUERY_CORRECTION_OFFSET, action = Action.GET) +public class QueryCorrectionOffsetHeader extends TopicRequestHeader { + @RocketMQResource(value = ResourceType.GROUP, splitter = ",") + private String filterGroups; + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String compareGroup; + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + private String topic; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getFilterGroups() { + return filterGroups; + } + + public void setFilterGroups(String filterGroups) { + this.filterGroups = filterGroups; + } + + public String getCompareGroup() { + return compareGroup; + } + + public void setCompareGroup(String compareGroup) { + this.compareGroup = compareGroup; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryMessageRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryMessageRequestHeader.java new file mode 100644 index 0000000..1d2a53a --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryMessageRequestHeader.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: QueryMessageRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.QUERY_MESSAGE, action = {Action.SUB, Action.GET}) +public class QueryMessageRequestHeader extends TopicRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + private String topic; + @CFNotNull + private String key; + @CFNotNull + private Integer maxNum; + @CFNotNull + private Long beginTimestamp; + @CFNotNull + private Long endTimestamp; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public Integer getMaxNum() { + return maxNum; + } + + public void setMaxNum(Integer maxNum) { + this.maxNum = maxNum; + } + + public Long getBeginTimestamp() { + return beginTimestamp; + } + + public void setBeginTimestamp(Long beginTimestamp) { + this.beginTimestamp = beginTimestamp; + } + + public Long getEndTimestamp() { + return endTimestamp; + } + + public void setEndTimestamp(Long endTimestamp) { + this.endTimestamp = endTimestamp; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryMessageResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryMessageResponseHeader.java new file mode 100644 index 0000000..b1927c5 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryMessageResponseHeader.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: QueryMessageResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +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 QueryMessageResponseHeader implements CommandCustomHeader { + @CFNotNull + private Long indexLastUpdateTimestamp; + @CFNotNull + private Long indexLastUpdatePhyoffset; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public Long getIndexLastUpdateTimestamp() { + return indexLastUpdateTimestamp; + } + + public void setIndexLastUpdateTimestamp(Long indexLastUpdateTimestamp) { + this.indexLastUpdateTimestamp = indexLastUpdateTimestamp; + } + + public Long getIndexLastUpdatePhyoffset() { + return indexLastUpdatePhyoffset; + } + + public void setIndexLastUpdatePhyoffset(Long indexLastUpdatePhyoffset) { + this.indexLastUpdatePhyoffset = indexLastUpdatePhyoffset; + } +} 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 0000000..981c7e2 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QuerySubscriptionByConsumerRequestHeader.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. + */ + +/** + * $Id: QueryMessageRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.QUERY_SUBSCRIPTION_BY_CONSUMER, action = Action.GET) +public class QuerySubscriptionByConsumerRequestHeader extends TopicRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String group; + @RocketMQResource(ResourceType.TOPIC) + 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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryTopicConsumeByWhoRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryTopicConsumeByWhoRequestHeader.java new file mode 100644 index 0000000..ee86323 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryTopicConsumeByWhoRequestHeader.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. + */ + +/** + * $Id: QueryMessageRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; + +@RocketMQAction(value = RequestCode.QUERY_TOPIC_CONSUME_BY_WHO, action = Action.GET) +public class QueryTopicConsumeByWhoRequestHeader extends TopicRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + 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/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 0000000..fe6723d --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryTopicsByConsumerRequestHeader.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. + */ + +/** + * $Id: QueryMessageRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; + +@RocketMQAction(value = RequestCode.QUERY_TOPICS_BY_CONSUMER, action = Action.GET) +public class QueryTopicsByConsumerRequestHeader extends RpcRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + 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/RecallMessageRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/RecallMessageRequestHeader.java new file mode 100644 index 0000000..c298836 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/RecallMessageRequestHeader.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.header; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +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.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; + +@RocketMQAction(value = RequestCode.RECALL_MESSAGE, action = Action.PUB) +public class RecallMessageRequestHeader extends TopicRequestHeader { + @CFNullable + private String producerGroup; + + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + private String topic; + + @CFNotNull + private String recallHandle; + + @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 getRecallHandle() { + return recallHandle; + } + + public void setRecallHandle(String recallHandle) { + this.recallHandle = recallHandle; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("producerGroup", producerGroup) + .add("topic", topic) + .add("recallHandle", recallHandle) + .toString(); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/RecallMessageResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/RecallMessageResponseHeader.java new file mode 100644 index 0000000..1833cfc --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/RecallMessageResponseHeader.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; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class RecallMessageResponseHeader implements CommandCustomHeader { + @CFNotNull + private String msgId; + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getMsgId() { + return msgId; + } + + public void setMsgId(String msgId) { + this.msgId = msgId; + } +} 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 0000000..2786b72 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/RemoveBrokerRequestHeader.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; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +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.RequestCode; + +@RocketMQAction(value = RequestCode.REMOVE_BROKER, resource = ResourceType.CLUSTER,action = Action.UPDATE) +public class RemoveBrokerRequestHeader implements CommandCustomHeader { + @CFNotNull + private String brokerName; + @CFNotNull + @RocketMQResource(ResourceType.CLUSTER) + 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 0000000..1a232bb --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ReplyMessageRequestHeader.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.remoting.protocol.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +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; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT, action = Action.SUB) +public class ReplyMessageRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String producerGroup; + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + 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 0000000..801ad08 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ResetMasterFlushOffsetHeader.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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +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.RequestCode; + +@RocketMQAction(value = RequestCode.RESET_MASTER_FLUSH_OFFSET, resource = ResourceType.CLUSTER, action = Action.UPDATE) +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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ResetOffsetRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ResetOffsetRequestHeader.java new file mode 100644 index 0000000..f72fe57 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ResetOffsetRequestHeader.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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.INVOKE_BROKER_TO_RESET_OFFSET, action = Action.UPDATE) +public class ResetOffsetRequestHeader extends TopicQueueRequestHeader { + + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String group; + + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + private String topic; + + private int queueId = -1; + + private Long offset; + + @CFNotNull + private long timestamp; + + @CFNotNull + private boolean isForce; + + 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 long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public boolean isForce() { + return isForce; + } + + 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 0000000..923fd37 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ResumeCheckHalfMessageRequestHeader.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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.RESUME_CHECK_HALF_MESSAGE, action = Action.UPDATE) +public class ResumeCheckHalfMessageRequestHeader implements CommandCustomHeader { + + @RocketMQResource(ResourceType.TOPIC) + private String topic; + @CFNullable + private String msgId; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + 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; + } + + @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 0000000..bbefa8c --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SearchOffsetRequestHeader.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. + */ + +/** + * $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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +@RocketMQAction(value = RequestCode.SEARCH_OFFSET_BY_TIMESTAMP, action = Action.GET) +public class SearchOffsetRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + 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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SearchOffsetResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SearchOffsetResponseHeader.java new file mode 100644 index 0000000..fe40062 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SearchOffsetResponseHeader.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. + */ + +/** + * $Id: SearchOffsetResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +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 SearchOffsetResponseHeader implements CommandCustomHeader { + @CFNotNull + private Long offset; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public Long getOffset() { + return offset; + } + + public void setOffset(Long offset) { + this.offset = offset; + } +} 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 0000000..2857fb5 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SendMessageRequestHeader.java @@ -0,0 +1,222 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +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; + +@RocketMQAction(value = RequestCode.SEND_MESSAGE, action = Action.PUB) +public class SendMessageRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + private String producerGroup; + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + 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; + @CFNullable + private Boolean batch; + 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() { + if (null == reconsumeTimes) { + return 0; + } + return reconsumeTimes; + } + + public void setReconsumeTimes(Integer reconsumeTimes) { + this.reconsumeTimes = reconsumeTimes; + } + + public boolean isUnitMode() { + if (null == unitMode) { + return false; + } + 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() { + if (null == batch) { + return false; + } + 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 = request.decodeCommandCustomHeader(SendMessageRequestHeaderV2.class); + case RequestCode.SEND_MESSAGE: + if (null == requestHeaderV2) { + requestHeader = 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 0000000..4812e90 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SendMessageRequestHeaderV2.java @@ -0,0 +1,348 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +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; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +/** + * Use short variable name to speed up FastJson deserialization process. + */ +@RocketMQAction(value = RequestCode.SEND_MESSAGE_V2, action = Action.PUB) +public class SendMessageRequestHeaderV2 extends TopicQueueRequestHeader implements CommandCustomHeader, FastCodesHeader { + @CFNotNull + private String a; // producerGroup; + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + 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; + + 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 0000000..7563b91 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SendMessageResponseHeader.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. + */ + +/** + * $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; + private String recallHandle; + + @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); + writeIfNotNull(out, "recallHandle", recallHandle); + } + + @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; + } + + str = fields.get("recallHandle"); + if (str != null) { + this.recallHandle = 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; + } + + public String getRecallHandle() { + return recallHandle; + } + + public void setRecallHandle(String recallHandle) { + this.recallHandle = recallHandle; + } +} 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 0000000..d8c22e3 --- /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 0000000..8a1bf4b --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UnlockBatchMqRequestHeader.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.protocol.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; + +@RocketMQAction(value = RequestCode.UNLOCK_BATCH_MQ, action = Action.SUB) +public class UnlockBatchMqRequestHeader extends RpcRequestHeader { + @Override + public void checkFields() throws RemotingCommandException { + + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UnregisterClientRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UnregisterClientRequestHeader.java new file mode 100644 index 0000000..0fc4c14 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UnregisterClientRequestHeader.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.remoting.protocol.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +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; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.UNREGISTER_CLIENT, action = {Action.PUB, Action.SUB}) +public class UnregisterClientRequestHeader extends RpcRequestHeader { + @CFNotNull + private String clientID; + + @CFNullable + private String producerGroup; + @CFNullable + @RocketMQResource(ResourceType.GROUP) + private String consumerGroup; + + public String getClientID() { + return clientID; + } + + public void setClientID(String clientID) { + this.clientID = clientID; + } + + public String getProducerGroup() { + return producerGroup; + } + + public void setProducerGroup(String producerGroup) { + this.producerGroup = producerGroup; + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + @Override + public void checkFields() throws RemotingCommandException { + + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UnregisterClientResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UnregisterClientResponseHeader.java new file mode 100644 index 0000000..f7347c5 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UnregisterClientResponseHeader.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.remoting.protocol.header; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class UnregisterClientResponseHeader implements CommandCustomHeader { + + @Override + public void checkFields() throws RemotingCommandException { + + } + +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateAclRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateAclRequestHeader.java new file mode 100644 index 0000000..8e6a9a2 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateAclRequestHeader.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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.AUTH_UPDATE_ACL, resource = ResourceType.CLUSTER, action = Action.UPDATE) +public class UpdateAclRequestHeader implements CommandCustomHeader { + + private String subject; + + public UpdateAclRequestHeader() { + } + + public UpdateAclRequestHeader(String subject) { + this.subject = subject; + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getSubject() { + return subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateConsumerOffsetRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateConsumerOffsetRequestHeader.java new file mode 100644 index 0000000..d22139f --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateConsumerOffsetRequestHeader.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. + */ + +/** + * $Id: UpdateConsumerOffsetRequestHeader.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.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +@RocketMQAction(value = RequestCode.UPDATE_CONSUMER_OFFSET, action = Action.SUB) +public class UpdateConsumerOffsetRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String consumerGroup; + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + private String topic; + @CFNotNull + private Integer queueId; + @CFNotNull + private Long commitOffset; + + @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 Long getCommitOffset() { + return commitOffset; + } + + 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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateConsumerOffsetResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateConsumerOffsetResponseHeader.java new file mode 100644 index 0000000..13b5b87 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateConsumerOffsetResponseHeader.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. + */ + +/** + * $Id: UpdateConsumerOffsetResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class UpdateConsumerOffsetResponseHeader implements CommandCustomHeader { + @Override + public void checkFields() throws RemotingCommandException { + + } +} 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 0000000..d46c79a --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateGroupForbiddenRequestHeader.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. + */ + +/** + * $Id: CreateTopicRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.UPDATE_AND_GET_GROUP_FORBIDDEN, action = Action.UPDATE) +public class UpdateGroupForbiddenRequestHeader extends TopicRequestHeader { + @CFNotNull + @RocketMQResource(ResourceType.GROUP) + private String group; + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) + 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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateUserRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateUserRequestHeader.java new file mode 100644 index 0000000..68d0341 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateUserRequestHeader.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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.AUTH_UPDATE_USER, resource = ResourceType.CLUSTER, action = Action.UPDATE) +public class UpdateUserRequestHeader implements CommandCustomHeader { + + private String username; + + public UpdateUserRequestHeader() { + } + + public UpdateUserRequestHeader(String username) { + this.username = username; + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ViewBrokerStatsDataRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ViewBrokerStatsDataRequestHeader.java new file mode 100644 index 0000000..5bd8b0e --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ViewBrokerStatsDataRequestHeader.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.remoting.protocol.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +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.RequestCode; + +@RocketMQAction(value = RequestCode.VIEW_BROKER_STATS_DATA, resource = ResourceType.CLUSTER, action = Action.GET) +public class ViewBrokerStatsDataRequestHeader implements CommandCustomHeader { + @CFNotNull + private String statsName; + @CFNotNull + private String statsKey; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getStatsName() { + return statsName; + } + + public void setStatsName(String statsName) { + this.statsName = statsName; + } + + public String getStatsKey() { + return statsKey; + } + + public void setStatsKey(String statsKey) { + this.statsKey = statsKey; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ViewMessageRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ViewMessageRequestHeader.java new file mode 100644 index 0000000..2dff436 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ViewMessageRequestHeader.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. + */ + +/** + * $Id: ViewMessageRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +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.RequestCode; + +@RocketMQAction(value = RequestCode.VIEW_MESSAGE_BY_ID, action = Action.GET) +public class ViewMessageRequestHeader implements CommandCustomHeader { + @RocketMQResource(ResourceType.TOPIC) + private String topic; + @CFNotNull + private Long offset; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public Long getOffset() { + return offset; + } + + public void setOffset(Long offset) { + this.offset = offset; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ViewMessageResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ViewMessageResponseHeader.java new file mode 100644 index 0000000..94484e0 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ViewMessageResponseHeader.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. + */ + +/** + * $Id: ViewMessageResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class ViewMessageResponseHeader implements CommandCustomHeader { + + @Override + public void checkFields() throws 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 0000000..89a12b7 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/AlterSyncStateSetRequestHeader.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.header.controller; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.CONTROLLER_ALTER_SYNC_STATE_SET, resource = ResourceType.CLUSTER, action = Action.UPDATE) +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 0000000..012197c --- /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 0000000..e8b596a --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/ElectMasterRequestHeader.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.remoting.protocol.header.controller; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +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.RequestCode; + +@RocketMQAction(value = RequestCode.CONTROLLER_ELECT_MASTER, resource = ResourceType.CLUSTER, action = Action.UPDATE) +public class ElectMasterRequestHeader implements CommandCustomHeader { + + @CFNotNull + @RocketMQResource(ResourceType.CLUSTER) + 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 0000000..aaf3b10 --- /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 0000000..9ae6c7c --- /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 0000000..8097f37 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/GetReplicaInfoRequestHeader.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.header.controller; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.CONTROLLER_GET_REPLICA_INFO, resource = ResourceType.CLUSTER, action = Action.GET) +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 0000000..f7aa49e --- /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 0000000..2ab87f8 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/admin/CleanControllerBrokerDataRequestHeader.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.remoting.protocol.header.controller.admin; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +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.RequestCode; + +@RocketMQAction(value = RequestCode.CLEAN_BROKER_DATA, resource = ResourceType.CLUSTER, action = Action.UPDATE) +public class CleanControllerBrokerDataRequestHeader implements CommandCustomHeader { + + @CFNullable + @RocketMQResource(ResourceType.CLUSTER) + 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 0000000..55222b5 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/register/ApplyBrokerIdRequestHeader.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.header.controller.register; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.CONTROLLER_APPLY_BROKER_ID, resource = ResourceType.CLUSTER, action = Action.UPDATE) +public class ApplyBrokerIdRequestHeader implements CommandCustomHeader { + + @RocketMQResource(ResourceType.CLUSTER) + 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 0000000..a7f100f --- /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 0000000..1ba0d30 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/register/GetNextBrokerIdRequestHeader.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.header.controller.register; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.CONTROLLER_GET_NEXT_BROKER_ID, resource = ResourceType.CLUSTER, action = Action.GET) +public class GetNextBrokerIdRequestHeader implements CommandCustomHeader { + + @RocketMQResource(ResourceType.CLUSTER) + 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 0000000..7d62722 --- /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 0000000..38247c3 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/register/RegisterBrokerToControllerRequestHeader.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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.CONTROLLER_REGISTER_BROKER, resource = ResourceType.CLUSTER, action = Action.UPDATE) +public class RegisterBrokerToControllerRequestHeader implements CommandCustomHeader { + + @RocketMQResource(ResourceType.CLUSTER) + 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 0000000..66bf0e4 --- /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 0000000..a250def --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/AddWritePermOfBrokerRequestHeader.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.header.namesrv; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +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.RequestCode; + +@RocketMQAction(value = RequestCode.ADD_WRITE_PERM_OF_BROKER, resource = ResourceType.CLUSTER, action = Action.UPDATE) +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 0000000..50bf6a9 --- /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 0000000..7566afa --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/BrokerHeartbeatRequestHeader.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.namesrv; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +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.RequestCode; + +@RocketMQAction(value = RequestCode.BROKER_HEARTBEAT, resource = ResourceType.CLUSTER, action = Action.UPDATE) +public class BrokerHeartbeatRequestHeader implements CommandCustomHeader { + @CFNotNull + @RocketMQResource(ResourceType.CLUSTER) + 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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/DeleteKVConfigRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/DeleteKVConfigRequestHeader.java new file mode 100644 index 0000000..ab747a5 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/DeleteKVConfigRequestHeader.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.protocol.header.namesrv; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +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.RequestCode; + +@RocketMQAction(value = RequestCode.DELETE_KV_CONFIG, resource = ResourceType.CLUSTER, action = Action.UPDATE) +public class DeleteKVConfigRequestHeader implements CommandCustomHeader { + @CFNotNull + private String namespace; + @CFNotNull + private String key; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } +} 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 0000000..81d80f1 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/DeleteTopicFromNamesrvRequestHeader.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.remoting.protocol.header.namesrv; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; + +@RocketMQAction(value = RequestCode.DELETE_TOPIC_IN_NAMESRV, resource = ResourceType.CLUSTER, action = Action.UPDATE) +public class DeleteTopicFromNamesrvRequestHeader extends TopicRequestHeader { + @CFNotNull + private String topic; + + @RocketMQResource(ResourceType.CLUSTER) + 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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/GetKVConfigRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/GetKVConfigRequestHeader.java new file mode 100644 index 0000000..21bfe98 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/GetKVConfigRequestHeader.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.protocol.header.namesrv; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +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.RequestCode; + +@RocketMQAction(value = RequestCode.GET_KV_CONFIG, resource = ResourceType.CLUSTER, action = Action.GET) +public class GetKVConfigRequestHeader implements CommandCustomHeader { + @CFNotNull + private String namespace; + @CFNotNull + private String key; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/GetKVConfigResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/GetKVConfigResponseHeader.java new file mode 100644 index 0000000..e5b1113 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/GetKVConfigResponseHeader.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.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class GetKVConfigResponseHeader implements CommandCustomHeader { + @CFNullable + private String value; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/GetKVListByNamespaceRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/GetKVListByNamespaceRequestHeader.java new file mode 100644 index 0000000..85bc514 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/GetKVListByNamespaceRequestHeader.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.header.namesrv; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +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.RequestCode; + +@RocketMQAction(value = RequestCode.GET_KVLIST_BY_NAMESPACE, resource = ResourceType.CLUSTER, action = Action.GET) +public class GetKVListByNamespaceRequestHeader implements CommandCustomHeader { + @CFNotNull + private String namespace; + + @Override + public void checkFields() throws RemotingCommandException { + } + + 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/header/namesrv/GetRouteInfoRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/GetRouteInfoRequestHeader.java new file mode 100644 index 0000000..760c393 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/GetRouteInfoRequestHeader.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. + */ + +/** + * $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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +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.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; + +@RocketMQAction(value = RequestCode.GET_ROUTEINFO_BY_TOPIC, resource = ResourceType.CLUSTER, action = Action.GET) +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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/PutKVConfigRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/PutKVConfigRequestHeader.java new file mode 100644 index 0000000..a253996 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/PutKVConfigRequestHeader.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.namesrv; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +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.RequestCode; + +@RocketMQAction(value = RequestCode.PUT_KV_CONFIG, resource = ResourceType.CLUSTER, action = Action.UPDATE) +public class PutKVConfigRequestHeader implements CommandCustomHeader { + @CFNotNull + private String namespace; + @CFNotNull + private String key; + @CFNotNull + private String value; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} 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 0000000..26fc5df --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/QueryDataVersionRequestHeader.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.header.namesrv; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +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.RequestCode; + +@RocketMQAction(value = RequestCode.QUERY_DATA_VERSION, resource = ResourceType.CLUSTER, action = Action.GET) +public class QueryDataVersionRequestHeader implements CommandCustomHeader { + @CFNotNull + private String brokerName; + @CFNotNull + private String brokerAddr; + @CFNotNull + @RocketMQResource(ResourceType.CLUSTER) + 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 0000000..94e83ba --- /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 0000000..eb2889a --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/RegisterBrokerRequestHeader.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. + */ + +/** + * $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.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +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.RequestCode; + +@RocketMQAction(value = RequestCode.REGISTER_BROKER, resource = ResourceType.CLUSTER, action = Action.UPDATE) +public class RegisterBrokerRequestHeader implements CommandCustomHeader { + @CFNotNull + private String brokerName; + @CFNotNull + private String brokerAddr; + @CFNotNull + @RocketMQResource(ResourceType.CLUSTER) + 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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/RegisterBrokerResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/RegisterBrokerResponseHeader.java new file mode 100644 index 0000000..0e35187 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/RegisterBrokerResponseHeader.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.header.namesrv; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class RegisterBrokerResponseHeader implements CommandCustomHeader { + @CFNullable + private String haServerAddr; + @CFNullable + private String masterAddr; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getHaServerAddr() { + return haServerAddr; + } + + public void setHaServerAddr(String haServerAddr) { + this.haServerAddr = haServerAddr; + } + + public String getMasterAddr() { + return masterAddr; + } + + public void setMasterAddr(String masterAddr) { + this.masterAddr = masterAddr; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/RegisterOrderTopicRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/RegisterOrderTopicRequestHeader.java new file mode 100644 index 0000000..39bb833 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/RegisterOrderTopicRequestHeader.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. + */ + +/** + * $Id: RegisterOrderTopicRequestHeader.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.exception.RemotingCommandException; + +public class RegisterOrderTopicRequestHeader implements CommandCustomHeader { + @CFNotNull + private String topic; + @CFNotNull + private String orderTopicString; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getOrderTopicString() { + return orderTopicString; + } + + public void setOrderTopicString(String orderTopicString) { + this.orderTopicString = orderTopicString; + } +} 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 0000000..73893f6 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/RegisterTopicRequestHeader.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.header.namesrv; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +@RocketMQAction(value = RequestCode.REGISTER_TOPIC_IN_NAMESRV, resource = ResourceType.CLUSTER, action = Action.UPDATE) +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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/UnRegisterBrokerRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/UnRegisterBrokerRequestHeader.java new file mode 100644 index 0000000..800d608 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/UnRegisterBrokerRequestHeader.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. + */ + +/** + * $Id: UnRegisterBrokerRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header.namesrv; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; +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.RequestCode; + +@RocketMQAction(value = RequestCode.UNREGISTER_BROKER, resource = ResourceType.CLUSTER, action = Action.UPDATE) +public class UnRegisterBrokerRequestHeader implements CommandCustomHeader { + @CFNotNull + private String brokerName; + @CFNotNull + private String brokerAddr; + @CFNotNull + @RocketMQResource(ResourceType.CLUSTER) + 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/WipeWritePermOfBrokerRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/WipeWritePermOfBrokerRequestHeader.java new file mode 100644 index 0000000..e712d58 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/WipeWritePermOfBrokerRequestHeader.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.header.namesrv; + +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +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.RequestCode; + +@RocketMQAction(value = RequestCode.WIPE_WRITE_PERM_OF_BROKER, resource = ResourceType.CLUSTER, action = Action.UPDATE) +public class WipeWritePermOfBrokerRequestHeader 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/WipeWritePermOfBrokerResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/WipeWritePermOfBrokerResponseHeader.java new file mode 100644 index 0000000..bd09e4b --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/WipeWritePermOfBrokerResponseHeader.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 WipeWritePermOfBrokerResponseHeader implements CommandCustomHeader { + @CFNotNull + private Integer wipeTopicCount; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public Integer getWipeTopicCount() { + return wipeTopicCount; + } + + public void setWipeTopicCount(Integer wipeTopicCount) { + this.wipeTopicCount = wipeTopicCount; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/ConsumeType.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/ConsumeType.java new file mode 100644 index 0000000..fbcca5d --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/ConsumeType.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. + */ + +/** + * $Id: ConsumeType.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.heartbeat; + +public enum ConsumeType { + + CONSUME_ACTIVELY("PULL"), + + CONSUME_PASSIVELY("PUSH"), + + CONSUME_POP("POP"); + + private String typeCN; + + ConsumeType(String typeCN) { + this.typeCN = typeCN; + } + + public String getTypeCN() { + return typeCN; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/ConsumerData.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/ConsumerData.java new file mode 100644 index 0000000..fe1e8df --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/ConsumerData.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. + */ + +/** + * $Id: ConsumerData.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 org.apache.rocketmq.common.consumer.ConsumeFromWhere; + +public class ConsumerData { + private String groupName; + private ConsumeType consumeType; + private MessageModel messageModel; + private ConsumeFromWhere consumeFromWhere; + private Set subscriptionDataSet = new HashSet<>(); + private boolean unitMode; + + public String getGroupName() { + return groupName; + } + + public void setGroupName(String groupName) { + this.groupName = groupName; + } + + 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 Set getSubscriptionDataSet() { + return subscriptionDataSet; + } + + public void setSubscriptionDataSet(Set subscriptionDataSet) { + this.subscriptionDataSet = subscriptionDataSet; + } + + public boolean isUnitMode() { + return unitMode; + } + + public void setUnitMode(boolean isUnitMode) { + this.unitMode = isUnitMode; + } + + @Override + public String toString() { + return "ConsumerData [groupName=" + groupName + ", consumeType=" + consumeType + ", messageModel=" + + messageModel + ", consumeFromWhere=" + consumeFromWhere + ", unitMode=" + unitMode + + ", subscriptionDataSet=" + subscriptionDataSet + "]"; + } +} 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 0000000..f7b4b9f --- /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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/MessageModel.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/MessageModel.java new file mode 100644 index 0000000..11f2e6c --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/MessageModel.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. + */ + +/** + * $Id: MessageModel.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.heartbeat; + +/** + * Message model + */ +public enum MessageModel { + /** + * broadcast + */ + BROADCASTING("BROADCASTING"), + /** + * clustering + */ + CLUSTERING("CLUSTERING"); + + private String modeCN; + + MessageModel(String modeCN) { + this.modeCN = modeCN; + } + + public String getModeCN() { + return modeCN; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/ProducerData.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/ProducerData.java new file mode 100644 index 0000000..ebf5fc4 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/ProducerData.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. + */ + +/** + * $Id: ProducerData.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.heartbeat; + +public class ProducerData { + private String groupName; + + public String getGroupName() { + return groupName; + } + + public void setGroupName(String groupName) { + this.groupName = groupName; + } + + @Override + public String toString() { + return "ProducerData [groupName=" + groupName + "]"; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/SubscriptionData.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/SubscriptionData.java new file mode 100644 index 0000000..59088fc --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/SubscriptionData.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. + */ + +/** + * $Id: SubscriptionData.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +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 long subVersion = System.currentTimeMillis(); + private String expressionType = ExpressionType.TAG; + + @JSONField(serialize = false) + private String filterClassSource; + + public SubscriptionData() { + + } + + public SubscriptionData(String topic, String subString) { + super(); + this.topic = topic; + this.subString = subString; + } + + public String getFilterClassSource() { + return filterClassSource; + } + + public void setFilterClassSource(String filterClassSource) { + this.filterClassSource = filterClassSource; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getSubString() { + return subString; + } + + public void setSubString(String subString) { + this.subString = subString; + } + + public Set getTagsSet() { + return tagsSet; + } + + public void setTagsSet(Set tagsSet) { + this.tagsSet = tagsSet; + } + + public long getSubVersion() { + return subVersion; + } + + public void setSubVersion(long subVersion) { + this.subVersion = subVersion; + } + + public Set getCodeSet() { + return codeSet; + } + + public void setCodeSet(Set codeSet) { + this.codeSet = codeSet; + } + + public boolean isClassFilterMode() { + return classFilterMode; + } + + public void setClassFilterMode(boolean classFilterMode) { + this.classFilterMode = classFilterMode; + } + + public String getExpressionType() { + return expressionType; + } + + public void setExpressionType(String expressionType) { + this.expressionType = expressionType; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (classFilterMode ? 1231 : 1237); + result = prime * result + ((codeSet == null) ? 0 : codeSet.hashCode()); + result = prime * result + ((subString == null) ? 0 : subString.hashCode()); + result = prime * result + ((tagsSet == null) ? 0 : tagsSet.hashCode()); + result = prime * result + ((topic == null) ? 0 : topic.hashCode()); + result = prime * result + ((expressionType == null) ? 0 : expressionType.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; + SubscriptionData other = (SubscriptionData) obj; + if (classFilterMode != other.classFilterMode) + return false; + if (codeSet == null) { + if (other.codeSet != null) + return false; + } else if (!codeSet.equals(other.codeSet)) + return false; + if (subString == null) { + if (other.subString != null) + return false; + } else if (!subString.equals(other.subString)) + return false; + if (subVersion != other.subVersion) + return false; + if (tagsSet == null) { + if (other.tagsSet != null) + return false; + } else if (!tagsSet.equals(other.tagsSet)) + return false; + if (topic == null) { + if (other.topic != null) + return false; + } else if (!topic.equals(other.topic)) + return false; + if (expressionType == null) { + if (other.expressionType != null) + return false; + } else if (!expressionType.equals(other.expressionType)) + return false; + return true; + } + + @Override + public String toString() { + return "SubscriptionData [classFilterMode=" + classFilterMode + ", topic=" + topic + ", subString=" + + subString + ", tagsSet=" + tagsSet + ", codeSet=" + codeSet + ", subVersion=" + subVersion + + ", expressionType=" + expressionType + "]"; + } + + @Override + public int compareTo(SubscriptionData other) { + String thisValue = this.topic + "@" + this.subString; + String otherValue = other.topic + "@" + other.subString; + return thisValue.compareTo(otherValue); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/namesrv/RegisterBrokerResult.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/namesrv/RegisterBrokerResult.java new file mode 100644 index 0000000..edbed3e --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/namesrv/RegisterBrokerResult.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.namesrv; + +import org.apache.rocketmq.remoting.protocol.body.KVTable; + +public class RegisterBrokerResult { + private String haServerAddr; + private String masterAddr; + private KVTable kvTable; + + public String getHaServerAddr() { + return haServerAddr; + } + + public void setHaServerAddr(String haServerAddr) { + this.haServerAddr = haServerAddr; + } + + public String getMasterAddr() { + return masterAddr; + } + + public void setMasterAddr(String masterAddr) { + this.masterAddr = masterAddr; + } + + public KVTable getKvTable() { + return kvTable; + } + + public void setKvTable(KVTable kvTable) { + this.kvTable = kvTable; + } +} 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 0000000..de911d1 --- /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 0000000..0e43a6f --- /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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/route/QueueData.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/route/QueueData.java new file mode 100644 index 0000000..3678e40 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/route/QueueData.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. + */ + +/* + $Id: QueueData.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +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 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; + } + + public void setReadQueueNums(int readQueueNums) { + this.readQueueNums = readQueueNums; + } + + public int getWriteQueueNums() { + return writeQueueNums; + } + + public void setWriteQueueNums(int writeQueueNums) { + this.writeQueueNums = writeQueueNums; + } + + public int getPerm() { + return perm; + } + + public void setPerm(int perm) { + this.perm = perm; + } + + public int getTopicSysFlag() { + return topicSysFlag; + } + + public void setTopicSysFlag(int topicSysFlag) { + this.topicSysFlag = topicSysFlag; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((brokerName == null) ? 0 : brokerName.hashCode()); + result = prime * result + perm; + result = prime * result + readQueueNums; + result = prime * result + writeQueueNums; + result = prime * result + topicSysFlag; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + QueueData other = (QueueData) obj; + if (brokerName == null) { + if (other.brokerName != null) + return false; + } else if (!brokerName.equals(other.brokerName)) + return false; + if (perm != other.perm) + return false; + if (readQueueNums != other.readQueueNums) + return false; + if (writeQueueNums != other.writeQueueNums) + return false; + return topicSysFlag == other.topicSysFlag; + } + + @Override + public String toString() { + return "QueueData [brokerName=" + brokerName + ", readQueueNums=" + readQueueNums + + ", writeQueueNums=" + writeQueueNums + ", perm=" + perm + ", topicSysFlag=" + topicSysFlag + + "]"; + } + + @Override + public int compareTo(QueueData 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/TopicRouteData.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/route/TopicRouteData.java new file mode 100644 index 0000000..2ef9923 --- /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 0000000..0c5bbb6 --- /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 0000000..d136927 --- /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 0000000..81718c8 --- /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 0000000..5c6e4d2 --- /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 0000000..4325a42 --- /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 0000000..8cbbd59 --- /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 0000000..647669f --- /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 does 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 does 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 does 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 does 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 does not increase monotonically"); + } + if (item.computeMaxStaticQueueOffset() >= lastOffset) { + throw new RuntimeException("The max logic offset does 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 does not begin from 0"); + } + TopicConfig topicConfig = brokerConfigMap.get(item.getBname()); + if (topicConfig == null) { + throw new RuntimeException("The broker of item does 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 does 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 + " does 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 0000000..75522bf --- /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 0000000..a8cdc74 --- /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 0000000..937c99d --- /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 0000000..5d50990 --- /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 0000000..14d5e53 --- /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 0000000..f68b127 --- /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 0000000..2a77fa8 --- /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 0000000..bb5c307 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/SimpleSubscriptionData.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.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 Objects.equals(topic, that.topic) && Objects.equals(expressionType, that.expressionType) && Objects.equals(expression, that.expression); + } + + @Override + public int hashCode() { + return Objects.hash(topic, expressionType, expression); + } + + @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 0000000..85cbce9 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/SubscriptionGroupConfig.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.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 + (consumeMessageOrderly ? 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(consumeMessageOrderly, other.consumeMessageOrderly) + .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/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/topic/OffsetMovedEvent.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/topic/OffsetMovedEvent.java new file mode 100644 index 0000000..ee87716 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/topic/OffsetMovedEvent.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.topic; + +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class OffsetMovedEvent extends RemotingSerializable { + private String consumerGroup; + private MessageQueue messageQueue; + private long offsetRequest; + private long offsetNew; + + 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 long getOffsetRequest() { + return offsetRequest; + } + + public void setOffsetRequest(long offsetRequest) { + this.offsetRequest = offsetRequest; + } + + public long getOffsetNew() { + return offsetNew; + } + + public void setOffsetNew(long offsetNew) { + this.offsetNew = offsetNew; + } + + @Override + public String toString() { + return "OffsetMovedEvent [consumerGroup=" + consumerGroup + ", messageQueue=" + messageQueue + + ", offsetRequest=" + offsetRequest + ", offsetNew=" + offsetNew + "]"; + } +} 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 0000000..f59b5dd --- /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 0000000..d4962e0 --- /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 0000000..e2e2c52 --- /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 0000000..f1df83b --- /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 0000000..5675148 --- /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 0000000..c8b404d --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcClientImpl.java @@ -0,0 +1,348 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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())); + break; + 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 0000000..78a33b7 --- /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 0000000..dda918b --- /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 0000000..3bf06c1 --- /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 0000000..810d876 --- /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 0000000..d7e7b17 --- /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 0000000..f265dd5 --- /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 0000000..9f21c07 --- /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 0000000..25a189e --- /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 0000000..501247a --- /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 0000000..c39fd21 --- /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 new file mode 100644 index 0000000..d0da0eb --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/RemotingServerTest.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; + +import io.netty.channel.ChannelHandlerContext; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import org.apache.rocketmq.remoting.annotation.CFNullable; +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.exception.RemotingTooMuchRequestException; +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.NettyRequestProcessor; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.netty.ResponseFuture; +import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertNotNull; + +public class RemotingServerTest { + private static RemotingServer remotingServer; + private static RemotingClient remotingClient; + + public static RemotingServer createRemotingServer() throws InterruptedException { + NettyServerConfig config = new NettyServerConfig(); + RemotingServer remotingServer = new NettyRemotingServer(config); + remotingServer.registerProcessor(0, new NettyRequestProcessor() { + @Override + public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) { + request.setRemark("Hi " + ctx.channel().remoteAddress()); + return request; + } + + @Override + public boolean rejectRequest() { + return false; + } + }, Executors.newCachedThreadPool()); + + remotingServer.start(); + + return remotingServer; + } + + public static RemotingClient createRemotingClient() { + return createRemotingClient(new NettyClientConfig()); + } + + public static RemotingClient createRemotingClient(NettyClientConfig nettyClientConfig) { + RemotingClient client = new NettyRemotingClient(nettyClientConfig); + client.start(); + return client; + } + + @BeforeClass + public static void setup() throws InterruptedException { + remotingServer = createRemotingServer(); + remotingClient = createRemotingClient(); + } + + @AfterClass + public static void destroy() { + remotingClient.shutdown(); + remotingServer.shutdown(); + } + + @Test + public void testInvokeSync() throws InterruptedException, RemotingConnectException, + RemotingSendRequestException, RemotingTimeoutException { + RequestHeader requestHeader = new RequestHeader(); + requestHeader.setCount(1); + requestHeader.setMessageTitle("Welcome"); + RemotingCommand request = RemotingCommand.createRequestCommand(0, requestHeader); + RemotingCommand response = remotingClient.invokeSync("localhost:" + remotingServer.localListenPort(), request, 1000 * 3); + assertNotNull(response); + assertThat(response.getLanguage()).isEqualTo(LanguageCode.JAVA); + assertThat(response.getExtFields()).hasSize(2); + + } + + @Test + public void testInvokeOneway() throws InterruptedException, RemotingConnectException, + RemotingTimeoutException, RemotingTooMuchRequestException, RemotingSendRequestException { + + RemotingCommand request = RemotingCommand.createRequestCommand(0, null); + request.setRemark("messi"); + remotingClient.invokeOneway("localhost:" + remotingServer.localListenPort(), request, 1000 * 3); + } + + @Test + public void testInvokeAsync() throws InterruptedException, RemotingConnectException, + RemotingTimeoutException, RemotingTooMuchRequestException, RemotingSendRequestException { + + final CountDownLatch latch = new CountDownLatch(1); + RemotingCommand request = RemotingCommand.createRequestCommand(0, null); + request.setRemark("messi"); + remotingClient.invokeAsync("localhost:" + remotingServer.localListenPort(), request, 1000 * 3, new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + + @Override + public void operationSucceed(RemotingCommand response) { + latch.countDown(); + assertThat(response.getLanguage()).isEqualTo(LanguageCode.JAVA); + assertThat(response.getExtFields()).hasSize(2); + } + + @Override + public void operationFail(Throwable throwable) { + + } + }); + latch.await(); + } +} + +class RequestHeader implements CommandCustomHeader { + @CFNullable + private Integer count; + + @CFNullable + private String messageTitle; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public Integer getCount() { + return count; + } + + public void setCount(Integer count) { + this.count = count; + } + + public String getMessageTitle() { + return messageTitle; + } + + public void setMessageTitle(String messageTitle) { + this.messageTitle = messageTitle; + } +} + 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 0000000..43ff1e9 --- /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 new file mode 100644 index 0000000..a4890d7 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/TlsTest.java @@ -0,0 +1,388 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 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; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.TestName; +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; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_CLIENT_KEYPATH; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_CLIENT_TRUSTCERTPATH; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_SERVER_AUTHCLIENT; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_SERVER_CERTPATH; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_SERVER_KEYPASSWORD; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_SERVER_KEYPATH; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_SERVER_NEED_CLIENT_AUTH; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_SERVER_TRUSTCERTPATH; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.tlsClientAuthServer; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.tlsClientCertPath; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.tlsClientKeyPassword; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.tlsClientKeyPath; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.tlsClientTrustCertPath; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.tlsConfigFile; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.tlsMode; +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; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertNotNull; + +@RunWith(MockitoJUnitRunner.class) +public class TlsTest { + private RemotingServer remotingServer; + private RemotingClient remotingClient; + + @Rule + public TestName name = new TestName(); + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Before + public void setUp() throws InterruptedException { + tlsMode = TlsMode.ENFORCING; + tlsTestModeEnable = false; + tlsServerNeedClientAuth = "require"; + tlsServerKeyPath = getCertsPath("server.key"); + tlsServerCertPath = getCertsPath("server.pem"); + tlsServerAuthClient = true; + tlsServerTrustCertPath = getCertsPath("ca.pem"); + tlsClientKeyPath = getCertsPath("client.key"); + tlsClientCertPath = getCertsPath("client.pem"); + tlsClientAuthServer = true; + tlsClientTrustCertPath = getCertsPath("ca.pem"); + tlsClientKeyPassword = "1234"; + tlsServerKeyPassword = ""; + + NettyClientConfig clientConfig = new NettyClientConfig(); + clientConfig.setUseTLS(true); + + if ("serverRejectsUntrustedClientCert".equals(name.getMethodName())) { + // Create a client. Its credentials come from a CA that the server does not trust. The client + // trusts both test CAs to ensure the handshake failure is due to the server rejecting the client's cert. + tlsClientKeyPath = getCertsPath("badClient.key"); + tlsClientCertPath = getCertsPath("badClient.pem"); + } else if ("serverAcceptsUntrustedClientCert".equals(name.getMethodName())) { + tlsClientKeyPath = getCertsPath("badClient.key"); + tlsClientCertPath = getCertsPath("badClient.pem"); + tlsServerAuthClient = false; + } + else if ("noClientAuthFailure".equals(name.getMethodName())) { + //Clear the client cert config to ensure produce the handshake error + tlsClientKeyPath = ""; + tlsClientCertPath = ""; + } else if ("clientRejectsUntrustedServerCert".equals(name.getMethodName())) { + tlsServerKeyPath = getCertsPath("badServer.key"); + tlsServerCertPath = getCertsPath("badServer.pem"); + } else if ("clientAcceptsUntrustedServerCert".equals(name.getMethodName())) { + tlsServerKeyPath = getCertsPath("badServer.key"); + tlsServerCertPath = getCertsPath("badServer.pem"); + tlsClientAuthServer = false; + } else if ("serverNotNeedClientAuth".equals(name.getMethodName())) { + tlsServerNeedClientAuth = "none"; + tlsClientKeyPath = ""; + tlsClientCertPath = ""; + } else if ("serverWantClientAuth".equals(name.getMethodName())) { + tlsServerNeedClientAuth = "optional"; + } else if ("serverWantClientAuth_ButClientNoCert".equals(name.getMethodName())) { + tlsServerNeedClientAuth = "optional"; + tlsClientKeyPath = ""; + tlsClientCertPath = ""; + } else if ("serverAcceptsUnAuthClient".equals(name.getMethodName())) { + tlsMode = TlsMode.PERMISSIVE; + tlsClientKeyPath = ""; + tlsClientCertPath = ""; + clientConfig.setUseTLS(false); + } 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 + public void tearDown() { + remotingClient.shutdown(); + remotingServer.shutdown(); + tlsMode = TlsMode.PERMISSIVE; + } + + /** + * Tests that a client and a server configured using two-way SSL auth can successfully + * communicate with each other. + */ + @Test + 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(); + } + + @Test + public void serverWantClientAuth_ButClientNoCert() throws Exception { + requestThenAssertResponse(); + } + + @Test + public void serverAcceptsUnAuthClient() throws Exception { + requestThenAssertResponse(); + } + + @Test + public void disabledServerRejectsSSLClient() throws Exception { + try { + 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. + */ + @Test + public void serverRejectsUntrustedClientCert() throws Exception { + try { + RemotingCommand response = remotingClient.invokeSync(getServerAddress(), createRequest(), 1000 * 5); + failBecauseExceptionWasNotThrown(RemotingSendRequestException.class); + } catch (RemotingSendRequestException ignore) { + } + } + + @Test + public void serverAcceptsUntrustedClientCert() throws Exception { + requestThenAssertResponse(); +// Thread.sleep(1000000L); + } + + /** + * Tests that a server configured to require client authentication actually does require client + * authentication. + */ + @Test + public void noClientAuthFailure() throws Exception { + try { + RemotingCommand response = remotingClient.invokeSync(getServerAddress(), createRequest(), 1000 * 3); + failBecauseExceptionWasNotThrown(RemotingSendRequestException.class); + } catch (RemotingSendRequestException ignore) { + } + } + + /** + * Tests that a client configured using GrpcSslContexts refuses to talk to a server that has an + * an untrusted certificate. + */ + @Test + public void clientRejectsUntrustedServerCert() throws Exception { + try { + RemotingCommand response = remotingClient.invokeSync(getServerAddress(), createRequest(), 1000 * 3); + failBecauseExceptionWasNotThrown(RemotingSendRequestException.class); + } catch (RemotingSendRequestException ignore) { + } + } + + @Test + public void clientAcceptsUntrustedServerCert() throws Exception { + requestThenAssertResponse(); + } + + @Test + public void testTlsConfigThroughFile() throws Exception { + File file = tempFolder.newFile("tls.config"); + tlsTestModeEnable = true; + + tlsConfigFile = file.getAbsolutePath(); + + StringBuilder sb = new StringBuilder(); + sb.append(TLS_SERVER_NEED_CLIENT_AUTH + "=require\n"); + sb.append(TLS_SERVER_KEYPATH + "=/server.key\n"); + sb.append(TLS_SERVER_CERTPATH + "=/server.pem\n"); + sb.append(TLS_SERVER_KEYPASSWORD + "=2345\n"); + sb.append(TLS_SERVER_AUTHCLIENT + "=true\n"); + sb.append(TLS_SERVER_TRUSTCERTPATH + "=/ca.pem\n"); + sb.append(TLS_CLIENT_KEYPATH + "=/client.key\n"); + sb.append(TLS_CLIENT_KEYPASSWORD + "=1234\n"); + sb.append(TLS_CLIENT_CERTPATH + "=/client.pem\n"); + sb.append(TLS_CLIENT_AUTHSERVER + "=false\n"); + sb.append(TLS_CLIENT_TRUSTCERTPATH + "=/ca.pem\n"); + + writeStringToFile(file.getAbsolutePath(), sb.toString()); + TlsHelper.buildSslContext(false); + + assertThat(tlsServerNeedClientAuth).isEqualTo("require"); + assertThat(tlsServerKeyPath).isEqualTo("/server.key"); + assertThat(tlsServerCertPath).isEqualTo("/server.pem"); + assertThat(tlsServerKeyPassword).isEqualTo("2345"); + assertThat(tlsServerAuthClient).isEqualTo(true); + assertThat(tlsServerTrustCertPath).isEqualTo("/ca.pem"); + assertThat(tlsClientKeyPath).isEqualTo("/client.key"); + assertThat(tlsClientKeyPassword).isEqualTo("1234"); + assertThat(tlsClientCertPath).isEqualTo("/client.pem"); + assertThat(tlsClientAuthServer).isEqualTo(false); + assertThat(tlsClientTrustCertPath).isEqualTo("/ca.pem"); + + tlsConfigFile = "/notFound"; + } + + private static void writeStringToFile(String path, String content) { + try { + PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(path, true))); + out.println(content); + out.close(); + } catch (IOException ignore) { + } + } + + private static String getCertsPath(String 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() { + RequestHeader requestHeader = new RequestHeader(); + requestHeader.setCount(1); + requestHeader.setMessageTitle("Welcome"); + return RemotingCommand.createRequestCommand(0, requestHeader); + } + + private void requestThenAssertResponse() throws Exception { + 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 new file mode 100644 index 0000000..0e35acc --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/netty/FileRegionEncoderTest.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.netty; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.DefaultFileRegion; +import io.netty.channel.FileRegion; +import io.netty.channel.embedded.EmbeddedChannel; +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 { + + /** + * This unit test case ensures that {@link FileRegionEncoder} indeed wraps {@link FileRegion} to + * {@link ByteBuf}. + * @throws IOException if there is an error. + */ + @Test + public void testEncode() throws IOException { + FileRegionEncoder fileRegionEncoder = new FileRegionEncoder(); + EmbeddedChannel channel = new EmbeddedChannel(fileRegionEncoder); + File file = File.createTempFile(UUID.randomUUID().toString(), ".data"); + file.deleteOnExit(); + Random random = new Random(System.currentTimeMillis()); + int dataLength = 1 << 10; + byte[] data = new byte[dataLength]; + random.nextBytes(data); + write(file, data); + FileRegion fileRegion = new DefaultFileRegion(file, 0, dataLength); + Assert.assertEquals(0, fileRegion.transferred()); + Assert.assertEquals(dataLength, fileRegion.count()); + Assert.assertTrue(channel.writeOutbound(fileRegion)); + ByteBuf out = (ByteBuf) channel.readOutbound(); + byte[] arr = new byte[out.readableBytes()]; + out.getBytes(0, arr); + Assert.assertArrayEquals("Data should be identical", data, arr); + } + + /** + * Write byte array to the specified file. + * + * @param file File to write to. + * @param data byte array to write. + * @throws IOException in case there is an exception. + */ + private static void write(File file, byte[] data) throws IOException { + BufferedOutputStream bufferedOutputStream = null; + try { + bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(file, false)); + bufferedOutputStream.write(data); + bufferedOutputStream.flush(); + } finally { + if (null != bufferedOutputStream) { + bufferedOutputStream.close(); + } + } + } +} 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 0000000..8ddcdf3 --- /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 0000000..9c3a354 --- /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 0000000..bc74950 --- /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 new file mode 100644 index 0000000..dbbea86 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/netty/NettyRemotingAbstractTest.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.remoting.netty; + +import java.util.concurrent.Semaphore; +import org.apache.rocketmq.remoting.InvokeCallback; +import org.apache.rocketmq.remoting.common.SemaphoreReleaseOnlyOnce; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Spy; +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) +public class NettyRemotingAbstractTest { + @Spy + private NettyRemotingAbstract remotingAbstract = new NettyRemotingClient(new NettyClientConfig()); + + @Test + public void testProcessResponseCommand() throws InterruptedException { + final Semaphore semaphore = new Semaphore(0); + ResponseFuture responseFuture = new ResponseFuture(null, 1, 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); + } + + @Test + public void testProcessResponseCommand_NullCallBack() throws InterruptedException { + final Semaphore semaphore = new Semaphore(0); + ResponseFuture responseFuture = new ResponseFuture(null, 1, 3000, null, + new SemaphoreReleaseOnlyOnce(semaphore)); + + remotingAbstract.responseTable.putIfAbsent(1, responseFuture); + + RemotingCommand response = RemotingCommand.createResponseCommand(0, "Foo"); + response.setOpaque(1); + remotingAbstract.processResponseCommand(null, response); + + assertThat(semaphore.availablePermits()).isEqualTo(1); + } + + @Test + public void testProcessResponseCommand_RunCallBackInCurrentThread() throws InterruptedException { + final Semaphore semaphore = new Semaphore(0); + ResponseFuture responseFuture = new ResponseFuture(null, 1, 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); + when(remotingAbstract.getCallbackExecutor()).thenReturn(null); + + RemotingCommand response = RemotingCommand.createResponseCommand(0, "Foo"); + response.setOpaque(1); + remotingAbstract.processResponseCommand(null, response); + + // Acquire the release permit after call back finished in current thread + 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 new file mode 100644 index 0000000..456e7ec --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/netty/NettyRemotingClientTest.java @@ -0,0 +1,309 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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() { + ExecutorService customized = Executors.newCachedThreadPool(); + remotingClient.setCallbackExecutor(customized); + assertThat(remotingClient.getCallbackExecutor()).isEqualTo(customized); + } + + @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 0000000..c69fceb --- /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 0000000..0ab0d19 --- /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 0000000..eb623a9 --- /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 0000000..658f59e --- /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 0000000..7d31931 --- /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/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/ConsumeStatusTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/ConsumeStatusTest.java new file mode 100644 index 0000000..b685d31 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/ConsumeStatusTest.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; + +import org.apache.rocketmq.remoting.protocol.body.ConsumeStatus; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.within; + +public class ConsumeStatusTest { + + @Test + public void testFromJson() throws Exception { + ConsumeStatus cs = new ConsumeStatus(); + cs.setConsumeFailedTPS(10); + cs.setPullRT(100); + cs.setPullTPS(1000); + String json = RemotingSerializable.toJson(cs, true); + ConsumeStatus fromJson = RemotingSerializable.fromJson(json, ConsumeStatus.class); + assertThat(fromJson.getPullRT()).isCloseTo(cs.getPullRT(), within(0.0001)); + assertThat(fromJson.getPullTPS()).isCloseTo(cs.getPullTPS(), within(0.0001)); + assertThat(fromJson.getConsumeFailedTPS()).isCloseTo(cs.getConsumeFailedTPS(), within(0.0001)); + } + +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/DataVersionTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/DataVersionTest.java new file mode 100644 index 0000000..dccedde --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/DataVersionTest.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; + +import java.util.concurrent.atomic.AtomicLong; +import org.junit.Assert; +import org.junit.Test; + +public class DataVersionTest { + + @Test + public void testEquals() { + DataVersion dataVersion = new DataVersion(); + DataVersion other = new DataVersion(); + other.setTimestamp(dataVersion.getTimestamp()); + Assert.assertTrue(dataVersion.equals(other)); + } + + @Test + public void testEquals_falseWhenCounterDifferent() { + DataVersion dataVersion = new DataVersion(); + DataVersion other = new DataVersion(); + other.setCounter(new AtomicLong(1L)); + other.setTimestamp(dataVersion.getTimestamp()); + Assert.assertFalse(dataVersion.equals(other)); + } + + @Test + public void testEquals_falseWhenCounterDifferent2() { + DataVersion dataVersion = new DataVersion(); + DataVersion other = new DataVersion(); + other.setCounter(null); + other.setTimestamp(dataVersion.getTimestamp()); + Assert.assertFalse(dataVersion.equals(other)); + } + + @Test + public void testEquals_falseWhenCounterDifferent3() { + DataVersion dataVersion = new DataVersion(); + dataVersion.setCounter(null); + DataVersion other = new DataVersion(); + other.setTimestamp(dataVersion.getTimestamp()); + Assert.assertFalse(dataVersion.equals(other)); + } + + @Test + public void testEquals_trueWhenCountersBothNull() { + DataVersion dataVersion = new DataVersion(); + dataVersion.setCounter(null); + DataVersion other = new DataVersion(); + other.setCounter(null); + other.setTimestamp(dataVersion.getTimestamp()); + Assert.assertTrue(dataVersion.equals(other)); + } + + @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 0000000..e0fba12 --- /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 0000000..e1f9016 --- /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 0000000..2f7af7a --- /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 0000000..6460d80 --- /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 0000000..e63552f --- /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 new file mode 100644 index 0000000..b5a0d00 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RemotingCommandTest.java @@ -0,0 +1,378 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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; + +public class RemotingCommandTest { + @Test + public void testMarkProtocolType_JSONProtocolType() { + int source = 261; + SerializeType type = SerializeType.JSON; + + 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}); + } + + @Test + public void testMarkProtocolType_ROCKETMQProtocolType() { + int source = 16777215; + SerializeType type = SerializeType.ROCKETMQ; + 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}); + } + + @Test + public void testCreateRequestCommand_RegisterBroker() { + System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, "2333"); + + 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); + assertThat(cmd.getVersion()).isEqualTo(2333); + assertThat(cmd.getFlag() & 0x01).isEqualTo(0); //flag bit 0: 0 presents request + } + + @Test + public void testCreateResponseCommand_SuccessWithHeader() { + System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, "2333"); + + int code = RemotingSysResponseCode.SUCCESS; + String remark = "Sample remark"; + RemotingCommand cmd = RemotingCommand.createResponseCommand(code, remark, SampleCommandCustomHeader.class); + assertThat(cmd.getCode()).isEqualTo(code); + assertThat(cmd.getVersion()).isEqualTo(2333); + assertThat(cmd.getRemark()).isEqualTo(remark); + assertThat(cmd.getFlag() & 0x01).isEqualTo(1); //flag bit 0: 1 presents response + } + + @Test + public void testCreateResponseCommand_SuccessWithoutHeader() { + System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, "2333"); + + int code = RemotingSysResponseCode.SUCCESS; + String remark = "Sample remark"; + RemotingCommand cmd = RemotingCommand.createResponseCommand(code, remark); + assertThat(cmd.getCode()).isEqualTo(code); + assertThat(cmd.getVersion()).isEqualTo(2333); + assertThat(cmd.getRemark()).isEqualTo(remark); + assertThat(cmd.getFlag() & 0x01).isEqualTo(1); //flag bit 0: 1 presents response + } + + @Test + public void testCreateResponseCommand_FailToCreateCommand() { + System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, "2333"); + + int code = RemotingSysResponseCode.SUCCESS; + String remark = "Sample remark"; + RemotingCommand cmd = RemotingCommand.createResponseCommand(code, remark, CommandCustomHeader.class); + assertThat(cmd).isNull(); + } + + @Test + public void testCreateResponseCommand_SystemError() { + System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, "2333"); + + RemotingCommand cmd = RemotingCommand.createResponseCommand(SampleCommandCustomHeader.class); + assertThat(cmd.getCode()).isEqualTo(RemotingSysResponseCode.SYSTEM_ERROR); + assertThat(cmd.getVersion()).isEqualTo(2333); + assertThat(cmd.getRemark()).contains("not set any response code"); + assertThat(cmd.getFlag() & 0x01).isEqualTo(1); //flag bit 0: 1 presents response + } + + @Test + public void testEncodeAndDecode_EmptyBody() { + System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, "2333"); + + int code = 103; //org.apache.rocketmq.remoting.protocol.RequestCode.REGISTER_BROKER + CommandCustomHeader header = new SampleCommandCustomHeader(); + RemotingCommand cmd = RemotingCommand.createRequestCommand(code, header); + + ByteBuffer buffer = cmd.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 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"); + } + + } + + @Test + public void testEncodeAndDecode_FilledBody() { + System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, "2333"); + + 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}); + + ByteBuffer buffer = cmd.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 decodedCommand = null; + try { + decodedCommand = RemotingCommand.decode(buffer); + + 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.remoting.protocol.RequestCode.REGISTER_BROKER + CommandCustomHeader header = new ExtFieldsHeader(); + RemotingCommand cmd = RemotingCommand.createRequestCommand(code, header); + + cmd.addExtField("key", "value"); + + ByteBuffer buffer = cmd.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 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("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"); + } + + } + + @Test + public void testNotNullField() throws Exception { + RemotingCommand remotingCommand = new RemotingCommand(); + Method method = RemotingCommand.class.getDeclaredMethod("isFieldNullable", Field.class); + method.setAccessible(true); + + Field nullString = FieldTestClass.class.getDeclaredField("nullString"); + assertThat(method.invoke(remotingCommand, nullString)).isEqualTo(false); + + Field nullableString = FieldTestClass.class.getDeclaredField("nullable"); + assertThat(method.invoke(remotingCommand, nullableString)).isEqualTo(true); + + 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 { + @CFNotNull + String nullString = null; + + String nullable = null; + + @CFNotNull + String value = "NotNull"; +} + +class SampleCommandCustomHeader implements CommandCustomHeader { + @Override + public void checkFields() throws RemotingCommandException { + } +} + +class ExtFieldsHeader implements CommandCustomHeader { + private String stringValue = "bilibili"; + private int intValue = 2333; + private long longValue = 23333333L; + private boolean booleanValue = true; + private double doubleValue = 0.618; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getStringValue() { + return stringValue; + } + + public int getIntValue() { + return intValue; + } + + public long getLongValue() { + return longValue; + } + + public boolean isBooleanValue() { + return booleanValue; + } + + public double getDoubleValue() { + return doubleValue; + } + + @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 new file mode 100644 index 0000000..6bd8021 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RemotingSerializableTest.java @@ -0,0 +1,204 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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 java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RemotingSerializableTest { + @Test + public void testEncodeAndDecode_HeterogeneousClass() { + Sample sample = new Sample(); + + byte[] bytes = RemotingSerializable.encode(sample); + Sample decodedSample = RemotingSerializable.decode(bytes, Sample.class); + + assertThat(decodedSample).isEqualTo(sample); + } + + @Test + public void testToJson_normalString() { + RemotingSerializable serializable = new RemotingSerializable() { + private List stringList = Arrays.asList("a", "o", "e", "i", "u", "v"); + + public List getStringList() { + return stringList; + } + + public void setStringList(List stringList) { + this.stringList = stringList; + } + }; + + String string = serializable.toJson(); + + assertThat(string).isEqualTo("{\"stringList\":[\"a\",\"o\",\"e\",\"i\",\"u\",\"v\"]}"); + } + + @Test + public void testToJson_prettyString() { + RemotingSerializable serializable = new RemotingSerializable() { + private List stringList = Arrays.asList("a", "o", "e", "i", "u", "v"); + + public List getStringList() { + return stringList; + } + + public void setStringList(List stringList) { + this.stringList = stringList; + } + }; + + String prettyString = serializable.toJson(true); + + assertThat(prettyString).isEqualTo("{\n" + + "\t\"stringList\":[\n" + + "\t\t\"a\",\n" + + "\t\t\"o\",\n" + + "\t\t\"e\",\n" + + "\t\t\"i\",\n" + + "\t\t\"u\",\n" + + "\t\t\"v\"\n" + + "\t]\n" + + "}"); + } + + @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 { + private String stringValue = "string"; + private int intValue = 2333; + private Integer integerValue = 666; + private double[] doubleArray = new double[] {0.618, 1.618}; + private List stringList = Arrays.asList("a", "o", "e", "i", "u", "v"); + + public String getStringValue() { + return stringValue; + } + + public void setStringValue(String stringValue) { + this.stringValue = stringValue; + } + + public int getIntValue() { + return intValue; + } + + public void setIntValue(int intValue) { + this.intValue = intValue; + } + + public Integer getIntegerValue() { + return integerValue; + } + + public void setIntegerValue(Integer integerValue) { + this.integerValue = integerValue; + } + + public double[] getDoubleArray() { + return doubleArray; + } + + public void setDoubleArray(double[] doubleArray) { + this.doubleArray = doubleArray; + } + + public List getStringList() { + return stringList; + } + + public void setStringList(List stringList) { + this.stringList = stringList; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + Sample sample = (Sample) o; + + if (intValue != sample.intValue) + return false; + if (stringValue != null ? !stringValue.equals(sample.stringValue) : sample.stringValue != null) + return false; + if (integerValue != null ? !integerValue.equals(sample.integerValue) : sample.integerValue != null) + return false; + if (!Arrays.equals(doubleArray, sample.doubleArray)) + return false; + return stringList != null ? stringList.equals(sample.stringList) : sample.stringList == null; + + } + + @Override + public int hashCode() { + int result = stringValue != null ? stringValue.hashCode() : 0; + result = 31 * result + intValue; + result = 31 * result + (integerValue != null ? integerValue.hashCode() : 0); + result = 31 * result + Arrays.hashCode(doubleArray); + result = 31 * result + (stringList != null ? stringList.hashCode() : 0); + return result; + } +} \ No newline at end of file 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 0000000..b2ed1e3 --- /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 0000000..7457926 --- /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 new file mode 100644 index 0000000..7cf32d7 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RocketMQSerializableTest.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.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; + +public class RocketMQSerializableTest { + @Test + public void testRocketMQProtocolEncodeAndDecode_WithoutRemarkWithoutExtFields() { + System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, "2333"); + + //org.apache.rocketmq.remoting.protocol.RequestCode.REGISTER_BROKER + int code = 103; + RemotingCommand cmd = RemotingCommand.createRequestCommand(code, new SampleCommandCustomHeader()); + cmd.setSerializeTypeCurrentRPC(SerializeType.ROCKETMQ); + + byte[] result = RocketMQSerializable.rocketMQProtocolEncode(cmd); + int opaque = cmd.getOpaque(); + + assertThat(result).hasSize(21); + assertThat(parseToShort(result, 0)).isEqualTo((short) code); //code + assertThat(result[2]).isEqualTo(LanguageCode.JAVA.getCode()); //language + assertThat(parseToShort(result, 3)).isEqualTo((short) 2333); //version + assertThat(parseToInt(result, 9)).isEqualTo(0); //flag + assertThat(parseToInt(result, 13)).isEqualTo(0); //empty remark + assertThat(parseToInt(result, 17)).isEqualTo(0); //empty extFields + + 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.remoting.protocol.RequestCode.REGISTER_BROKER + int code = 103; + RemotingCommand cmd = RemotingCommand.createRequestCommand(code, + new SampleCommandCustomHeader()); + cmd.setSerializeTypeCurrentRPC(SerializeType.ROCKETMQ); + cmd.setRemark("Sample Remark"); + + byte[] result = RocketMQSerializable.rocketMQProtocolEncode(cmd); + int opaque = cmd.getOpaque(); + + assertThat(result).hasSize(34); + assertThat(parseToShort(result, 0)).isEqualTo((short) code); //code + assertThat(result[2]).isEqualTo(LanguageCode.JAVA.getCode()); //language + assertThat(parseToShort(result, 3)).isEqualTo((short) 2333); //version + assertThat(parseToInt(result, 9)).isEqualTo(0); //flag + assertThat(parseToInt(result, 13)).isEqualTo(13); //remark length + + byte[] remarkArray = new byte[13]; + System.arraycopy(result, 17, remarkArray, 0, 13); + assertThat(new String(remarkArray)).isEqualTo("Sample Remark"); + + assertThat(parseToInt(result, 30)).isEqualTo(0); //empty extFields + + 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() throws Exception { + System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, "2333"); + + //org.apache.rocketmq.remoting.protocol.RequestCode.REGISTER_BROKER + int code = 103; + RemotingCommand cmd = RemotingCommand.createRequestCommand(code, + new SampleCommandCustomHeader()); + cmd.setSerializeTypeCurrentRPC(SerializeType.ROCKETMQ); + cmd.addExtField("key", "value"); + + byte[] result = RocketMQSerializable.rocketMQProtocolEncode(cmd); + int opaque = cmd.getOpaque(); + + assertThat(result).hasSize(35); + assertThat(parseToShort(result, 0)).isEqualTo((short) code); //code + assertThat(result[2]).isEqualTo(LanguageCode.JAVA.getCode()); //language + assertThat(parseToShort(result, 3)).isEqualTo((short) 2333); //version + assertThat(parseToInt(result, 9)).isEqualTo(0); //flag + assertThat(parseToInt(result, 13)).isEqualTo(0); //empty remark + assertThat(parseToInt(result, 17)).isEqualTo(14); //extFields length + + byte[] extFieldsArray = new byte[14]; + System.arraycopy(result, 21, extFieldsArray, 0, 14); + HashMap extFields = + RocketMQSerializable.mapDeserialize(Unpooled.wrappedBuffer(extFieldsArray), extFieldsArray.length); + assertThat(extFields).contains(new HashMap.SimpleEntry("key", "value")); + + 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) { + return (short) (array[index] * 256 + array[++index]); + } + + private int parseToInt(byte[] array, int index) { + return array[index] * 16777216 + array[++index] * 65536 + array[++index] * 256 + + array[++index]; + } + + 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 0000000..10021c4 --- /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 0000000..bb828db --- /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 0000000..427a132 --- /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 0000000..cb1faef --- /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 0000000..402ca58 --- /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 0000000..5e9d385 --- /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 0000000..01a4506 --- /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 0000000..e9d2fee --- /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 0000000..f05de38 --- /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 0000000..6148213 --- /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 0000000..6ae3dbd --- /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 0000000..7e44d71 --- /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 0000000..6d62b07 --- /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 0000000..1157499 --- /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 0000000..b7d89a2 --- /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/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/filter/FilterAPITest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/filter/FilterAPITest.java new file mode 100644 index 0000000..002a1ba --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/filter/FilterAPITest.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.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; + +public class FilterAPITest { + private String topic = "FooBar"; + private String group = "FooBarGroup"; + private String subString = "TAG1 || Tag2 || tag3"; + + @Test + public void testBuildSubscriptionData() throws Exception { + SubscriptionData subscriptionData = + FilterAPI.buildSubscriptionData(topic, subString); + assertThat(subscriptionData.getTopic()).isEqualTo(topic); + assertThat(subscriptionData.getSubString()).isEqualTo(subString); + String[] tags = subString.split("\\|\\|"); + Set tagSet = new HashSet<>(); + for (String tag : tags) { + tagSet.add(tag.trim()); + } + assertThat(subscriptionData.getTagsSet()).isEqualTo(tagSet); + } + + @Test + public void testBuildTagSome() { + try { + SubscriptionData subscriptionData = FilterAPI.build( + "TOPIC", "A || B", ExpressionType.TAG + ); + + assertThat(subscriptionData).isNotNull(); + assertThat(subscriptionData.getTopic()).isEqualTo("TOPIC"); + assertThat(subscriptionData.getSubString()).isEqualTo("A || B"); + assertThat(ExpressionType.isTagType(subscriptionData.getExpressionType())).isTrue(); + + assertThat(subscriptionData.getTagsSet()).isNotNull(); + assertThat(subscriptionData.getTagsSet()).containsExactlyInAnyOrder("A", "B"); + } catch (Exception e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + } + + @Test + public void testBuildSQL() { + try { + SubscriptionData subscriptionData = FilterAPI.build( + "TOPIC", "a is not null", ExpressionType.SQL92 + ); + + assertThat(subscriptionData).isNotNull(); + assertThat(subscriptionData.getTopic()).isEqualTo("TOPIC"); + assertThat(subscriptionData.getExpressionType()).isEqualTo(ExpressionType.SQL92); + } catch (Exception e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testBuildSQLWithNullSubString() throws Exception { + FilterAPI.build("TOPIC", null, ExpressionType.SQL92); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/header/ExportRocksDBConfigToJsonRequestHeaderTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/header/ExportRocksDBConfigToJsonRequestHeaderTest.java new file mode 100644 index 0000000..bbe625a --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/header/ExportRocksDBConfigToJsonRequestHeaderTest.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.util.ArrayList; +import java.util.List; +import org.junit.Assert; +import org.junit.Test; + +public class ExportRocksDBConfigToJsonRequestHeaderTest { + @Test + public void configTypeTest() { + List configTypes = new ArrayList<>(); + configTypes.add(ExportRocksDBConfigToJsonRequestHeader.ConfigType.TOPICS); + configTypes.add(ExportRocksDBConfigToJsonRequestHeader.ConfigType.SUBSCRIPTION_GROUPS); + + String string = ExportRocksDBConfigToJsonRequestHeader.ConfigType.toString(configTypes); + + List newConfigTypes = ExportRocksDBConfigToJsonRequestHeader.ConfigType.fromString(string); + assert newConfigTypes.size() == 2; + assert configTypes.equals(newConfigTypes); + + List topics = ExportRocksDBConfigToJsonRequestHeader.ConfigType.fromString("topics"); + assert topics.size() == 1; + assert topics.get(0).equals(ExportRocksDBConfigToJsonRequestHeader.ConfigType.TOPICS); + + List mix = ExportRocksDBConfigToJsonRequestHeader.ConfigType.fromString("toPics; subScriptiongroups"); + assert mix.size() == 2; + assert mix.get(0).equals(ExportRocksDBConfigToJsonRequestHeader.ConfigType.TOPICS); + assert mix.get(1).equals(ExportRocksDBConfigToJsonRequestHeader.ConfigType.SUBSCRIPTION_GROUPS); + + Assert.assertThrows(IllegalArgumentException.class, () -> { + ExportRocksDBConfigToJsonRequestHeader.ConfigType.fromString("topics; subscription"); + }); + + } +} 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 0000000..8081f38 --- /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 0000000..b6a0d63 --- /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/GetConsumeStatsRequestHeaderTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/header/GetConsumeStatsRequestHeaderTest.java new file mode 100644 index 0000000..8004305 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/header/GetConsumeStatsRequestHeaderTest.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.remoting.protocol.header; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class GetConsumeStatsRequestHeaderTest { + + private GetConsumeStatsRequestHeader header; + + @Before + public void setUp() { + header = new GetConsumeStatsRequestHeader(); + } + + @Test + public void updateTopicList_NullTopicList_DoesNotUpdate() { + header.updateTopicList(null); + assertNull(header.getTopicList()); + } + + @Test + public void updateTopicList_EmptyTopicList_SetsEmptyString() { + header.updateTopicList(Collections.emptyList()); + assertNull(header.getTopicList()); + } + + @Test + public void updateTopicList_SingleTopic_SetsSingleTopicString() { + List topicList = Collections.singletonList("TopicA"); + header.updateTopicList(topicList); + assertEquals("TopicA;", header.getTopicList()); + } + + @Test + public void updateTopicList_MultipleTopics_SetsMultipleTopicsString() { + List topicList = Arrays.asList("TopicA", "TopicB", "TopicC"); + header.updateTopicList(topicList); + assertEquals("TopicA;TopicB;TopicC;", header.getTopicList()); + } + + @Test + public void updateTopicList_RepeatedTopics_SetsRepeatedTopicsString() { + List topicList = Arrays.asList("TopicA", "TopicA", "TopicB"); + header.updateTopicList(topicList); + assertEquals("TopicA;TopicA;TopicB;", header.getTopicList()); + } + + @Test + public void fetchTopicList_NullTopicList_ReturnsEmptyList() { + header.setTopicList(null); + List topicList = header.fetchTopicList(); + assertEquals(Collections.emptyList(), topicList); + + header.updateTopicList(new ArrayList<>()); + topicList = header.fetchTopicList(); + assertEquals(Collections.emptyList(), topicList); + } + + @Test + public void fetchTopicList_EmptyTopicList_ReturnsEmptyList() { + header.setTopicList(""); + List topicList = header.fetchTopicList(); + assertEquals(Collections.emptyList(), topicList); + } + + @Test + public void fetchTopicList_BlankTopicList_ReturnsEmptyList() { + header.setTopicList(" "); + List topicList = header.fetchTopicList(); + assertEquals(Collections.emptyList(), topicList); + } + + @Test + public void fetchTopicList_SingleTopic_ReturnsSingleTopicList() { + header.setTopicList("TopicA"); + List topicList = header.fetchTopicList(); + assertEquals(Collections.singletonList("TopicA"), topicList); + } + + @Test + public void fetchTopicList_MultipleTopics_ReturnsTopicList() { + header.setTopicList("TopicA;TopicB;TopicC"); + List topicList = header.fetchTopicList(); + assertEquals(Arrays.asList("TopicA", "TopicB", "TopicC"), topicList); + } + + @Test + public void fetchTopicList_TopicListEndsWithSeparator_ReturnsTopicList() { + header.setTopicList("TopicA;TopicB;"); + List topicList = header.fetchTopicList(); + assertEquals(Arrays.asList("TopicA", "TopicB"), topicList); + } + + @Test + public void fetchTopicList_TopicListStartsWithSeparator_ReturnsTopicList() { + header.setTopicList(";TopicA;TopicB"); + List topicList = header.fetchTopicList(); + assertEquals(Arrays.asList("TopicA", "TopicB"), topicList); + } +} 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 0000000..c817d8c --- /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 0000000..a0dd665 --- /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 0000000..9bee83a --- /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 0000000..6b8a139 --- /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 0000000..a12c9f8 --- /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 0000000..eb215b3 --- /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 0000000..0a0421b --- /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 0000000..c449e56 --- /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/subscription/SimpleSubscriptionDataTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/subscription/SimpleSubscriptionDataTest.java new file mode 100644 index 0000000..fa8e4ba --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/subscription/SimpleSubscriptionDataTest.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.subscription; + +import com.google.common.collect.Sets; +import java.util.Set; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SimpleSubscriptionDataTest { + @Test + public void testNotEqual() { + String topic = "test-topic"; + String expressionType = "TAG"; + String expression1 = "test-expression-1"; + String expression2 = "test-expression-2"; + SimpleSubscriptionData simpleSubscriptionData1 = new SimpleSubscriptionData(topic, expressionType, expression1, 1); + SimpleSubscriptionData simpleSubscriptionData2 = new SimpleSubscriptionData(topic, expressionType, expression2, 1); + assertThat(simpleSubscriptionData1.equals(simpleSubscriptionData2)).isFalse(); + } + + @Test + public void testEqual() { + String topic = "test-topic"; + String expressionType = "TAG"; + String expression1 = "test-expression-1"; + String expression2 = "test-expression-1"; + SimpleSubscriptionData simpleSubscriptionData1 = new SimpleSubscriptionData(topic, expressionType, expression1, 1); + SimpleSubscriptionData simpleSubscriptionData2 = new SimpleSubscriptionData(topic, expressionType, expression2, 1); + assertThat(simpleSubscriptionData1.equals(simpleSubscriptionData2)).isTrue(); + } + + @Test + public void testSetNotEqual() { + String topic = "test-topic"; + String expressionType = "TAG"; + String expression1 = "test-expression-1"; + String expression2 = "test-expression-2"; + Set set1 = Sets.newHashSet(new SimpleSubscriptionData(topic, expressionType, expression1, 1)); + Set set2 = Sets.newHashSet(new SimpleSubscriptionData(topic, expressionType, expression2, 1)); + assertThat(set1.equals(set2)).isFalse(); + } + + @Test + public void testSetEqual() { + String topic = "test-topic"; + String expressionType = "TAG"; + String expression1 = "test-expression-1"; + String expression2 = "test-expression-1"; + Set set1 = Sets.newHashSet(new SimpleSubscriptionData(topic, expressionType, expression1, 1)); + Set set2 = Sets.newHashSet(new SimpleSubscriptionData(topic, expressionType, expression2, 1)); + assertThat(set1.equals(set2)).isTrue(); + } +} \ No newline at end of file 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 0000000..c8d4ef9 --- /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/ClientMetadataTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/rpc/ClientMetadataTest.java new file mode 100644 index 0000000..a9f3885 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/rpc/ClientMetadataTest.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.remoting.rpc; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingInfo; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@RunWith(MockitoJUnitRunner.class) +public class ClientMetadataTest { + + private ClientMetadata clientMetadata; + + private final ConcurrentMap topicRouteTable = new ConcurrentHashMap<>(); + + private final ConcurrentMap> topicEndPointsTable = new ConcurrentHashMap<>(); + + private final ConcurrentMap> brokerAddrTable = new ConcurrentHashMap<>(); + + private final String defaultTopic = "defaultTopic"; + + private final String defaultBroker = "defaultBroker"; + + @Before + public void init() throws IllegalAccessException { + clientMetadata = new ClientMetadata(); + + FieldUtils.writeDeclaredField(clientMetadata, "topicRouteTable", topicRouteTable, true); + FieldUtils.writeDeclaredField(clientMetadata, "topicEndPointsTable", topicEndPointsTable, true); + FieldUtils.writeDeclaredField(clientMetadata, "brokerAddrTable", brokerAddrTable, true); + } + + @Test + public void testGetBrokerNameFromMessageQueue() { + MessageQueue mq1 = new MessageQueue(defaultTopic, "broker0", 0); + MessageQueue mq2 = new MessageQueue(defaultTopic, "broker1", 0); + ConcurrentMap messageQueueMap = new ConcurrentHashMap<>(); + messageQueueMap.put(mq1, "broker0"); + messageQueueMap.put(mq2, "broker1"); + topicEndPointsTable.put(defaultTopic, messageQueueMap); + + String actual = clientMetadata.getBrokerNameFromMessageQueue(mq1); + assertEquals("broker0", actual); + } + + @Test + public void testGetBrokerNameFromMessageQueueNotFound() { + MessageQueue mq = new MessageQueue("topic1", "broker0", 0); + topicEndPointsTable.put(defaultTopic, new ConcurrentHashMap<>()); + + String actual = clientMetadata.getBrokerNameFromMessageQueue(mq); + assertEquals("broker0", actual); + } + + @Test + public void testFindMasterBrokerAddrNotFound() { + assertNull(clientMetadata.findMasterBrokerAddr(defaultBroker)); + } + + @Test + public void testFindMasterBrokerAddr() { + String defaultBrokerAddr = "127.0.0.1:10911"; + brokerAddrTable.put(defaultBroker, new HashMap<>()); + brokerAddrTable.get(defaultBroker).put(0L, defaultBrokerAddr); + + String actual = clientMetadata.findMasterBrokerAddr(defaultBroker); + assertEquals(defaultBrokerAddr, actual); + } + + @Test + public void testTopicRouteData2EndpointsForStaticTopicNotFound() { + TopicRouteData topicRouteData = new TopicRouteData(); + topicRouteData.setTopicQueueMappingByBroker(null); + + ConcurrentMap actual = ClientMetadata.topicRouteData2EndpointsForStaticTopic(defaultTopic, topicRouteData); + assertTrue(actual.isEmpty()); + } + + @Test + public void testTopicRouteData2EndpointsForStaticTopic() { + TopicRouteData topicRouteData = new TopicRouteData(); + Map mappingInfos = new HashMap<>(); + TopicQueueMappingInfo info = new TopicQueueMappingInfo(); + info.setScope("scope"); + info.setCurrIdMap(new ConcurrentHashMap<>()); + info.getCurrIdMap().put(0, 0); + info.setTotalQueues(1); + info.setBname("bname"); + mappingInfos.put(defaultBroker, info); + topicRouteData.setTopicQueueMappingByBroker(mappingInfos); + + ConcurrentMap actual = ClientMetadata.topicRouteData2EndpointsForStaticTopic(defaultTopic, topicRouteData); + assertEquals(1, actual.size()); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/rpc/RpcClientImplTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/rpc/RpcClientImplTest.java new file mode 100644 index 0000000..c33511a --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/rpc/RpcClientImplTest.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.remoting.rpc; + +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.InvokeCallback; +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.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; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.concurrent.Future; + +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.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class RpcClientImplTest { + + @Mock + private RemotingClient remotingClient; + + @Mock + private ClientMetadata clientMetadata; + + private RpcClientImpl rpcClient; + + private MessageQueue mq; + + @Mock + private RpcRequest request; + + private final long defaultTimeout = 3000L; + + @Before + public void init() throws IllegalAccessException { + rpcClient = new RpcClientImpl(clientMetadata, remotingClient); + + String defaultBroker = "brokerName"; + mq = new MessageQueue("defaultTopic", defaultBroker, 0); + RpcRequestHeader header = mock(RpcRequestHeader.class); + when(request.getHeader()).thenReturn(header); + when(clientMetadata.getBrokerNameFromMessageQueue(mq)).thenReturn(defaultBroker); + when(clientMetadata.findMasterBrokerAddr(any())).thenReturn("127.0.0.1:10911"); + } + + @Test + public void testInvoke_PULL_MESSAGE() throws Exception { + when(request.getCode()).thenReturn(RequestCode.PULL_MESSAGE); + + doAnswer(invocation -> { + InvokeCallback callback = invocation.getArgument(3); + RemotingCommand response = mock(RemotingCommand.class); + when(response.getBody()).thenReturn("success".getBytes()); + PullMessageResponseHeader responseHeader = mock(PullMessageResponseHeader.class); + when(response.decodeCommandCustomHeader(PullMessageResponseHeader.class)).thenReturn(responseHeader); + callback.operationSucceed(response); + return null; + }).when(remotingClient).invokeAsync( + any(), + any(RemotingCommand.class), + anyLong(), + any(InvokeCallback.class)); + + Future future = rpcClient.invoke(mq, request, defaultTimeout); + RpcResponse actual = future.get(); + + assertEquals(ResponseCode.SUCCESS, actual.getCode()); + assertEquals("success", new String((byte[]) actual.getBody())); + } + + @Test + public void testInvoke_GET_MIN_OFFSET() throws Exception { + when(request.getCode()).thenReturn(RequestCode.GET_MIN_OFFSET); + + RemotingCommand responseCommand = mock(RemotingCommand.class); + when(responseCommand.getBody()).thenReturn("1".getBytes()); + GetMinOffsetResponseHeader responseHeader = mock(GetMinOffsetResponseHeader.class); + when(responseCommand.decodeCommandCustomHeader(GetMinOffsetResponseHeader.class)).thenReturn(responseHeader); + when(remotingClient.invokeSync(any(), any(RemotingCommand.class), anyLong())).thenReturn(responseCommand); + + Future future = rpcClient.invoke(mq, request, defaultTimeout); + RpcResponse actual = future.get(); + + assertEquals(ResponseCode.SUCCESS, actual.getCode()); + assertEquals("1", new String((byte[]) actual.getBody())); + } + + @Test + public void testInvoke_GET_MAX_OFFSET() throws Exception { + when(request.getCode()).thenReturn(RequestCode.GET_MAX_OFFSET); + + RemotingCommand responseCommand = mock(RemotingCommand.class); + when(responseCommand.getBody()).thenReturn("1000".getBytes()); + GetMaxOffsetResponseHeader responseHeader = mock(GetMaxOffsetResponseHeader.class); + when(responseCommand.decodeCommandCustomHeader(GetMaxOffsetResponseHeader.class)).thenReturn(responseHeader); + when(remotingClient.invokeSync(any(), any(RemotingCommand.class), anyLong())).thenReturn(responseCommand); + + Future future = rpcClient.invoke(mq, request, defaultTimeout); + RpcResponse actual = future.get(); + + assertEquals(ResponseCode.SUCCESS, actual.getCode()); + assertEquals("1000", new String((byte[]) actual.getBody())); + } + + @Test + public void testInvoke_SEARCH_OFFSET_BY_TIMESTAMP() throws Exception { + when(request.getCode()).thenReturn(RequestCode.SEARCH_OFFSET_BY_TIMESTAMP); + + RemotingCommand responseCommand = mock(RemotingCommand.class); + when(responseCommand.getBody()).thenReturn("1000".getBytes()); + SearchOffsetResponseHeader responseHeader = mock(SearchOffsetResponseHeader.class); + when(responseCommand.decodeCommandCustomHeader(SearchOffsetResponseHeader.class)).thenReturn(responseHeader); + when(remotingClient.invokeSync(any(), any(RemotingCommand.class), anyLong())).thenReturn(responseCommand); + + Future future = rpcClient.invoke(mq, request, defaultTimeout); + RpcResponse actual = future.get(); + + assertEquals(ResponseCode.SUCCESS, actual.getCode()); + assertEquals("1000", new String((byte[]) actual.getBody())); + } + + @Test + public void testInvoke_GET_EARLIEST_MSG_STORETIME() throws Exception { + when(request.getCode()).thenReturn(RequestCode.GET_EARLIEST_MSG_STORETIME); + + RemotingCommand responseCommand = mock(RemotingCommand.class); + when(responseCommand.getBody()).thenReturn("10000".getBytes()); + GetEarliestMsgStoretimeResponseHeader responseHeader = mock(GetEarliestMsgStoretimeResponseHeader.class); + when(responseCommand.decodeCommandCustomHeader(GetEarliestMsgStoretimeResponseHeader.class)).thenReturn(responseHeader); + when(remotingClient.invokeSync(any(), any(RemotingCommand.class), anyLong())).thenReturn(responseCommand); + + Future future = rpcClient.invoke(mq, request, defaultTimeout); + RpcResponse actual = future.get(); + + assertEquals(ResponseCode.SUCCESS, actual.getCode()); + assertEquals("10000", new String((byte[]) actual.getBody())); + } + + @Test + public void testInvoke_QUERY_CONSUMER_OFFSET() throws Exception { + when(request.getCode()).thenReturn(RequestCode.QUERY_CONSUMER_OFFSET); + + RemotingCommand responseCommand = mock(RemotingCommand.class); + when(responseCommand.getBody()).thenReturn("1000".getBytes()); + QueryConsumerOffsetResponseHeader responseHeader = mock(QueryConsumerOffsetResponseHeader.class); + when(responseCommand.decodeCommandCustomHeader(QueryConsumerOffsetResponseHeader.class)).thenReturn(responseHeader); + when(remotingClient.invokeSync(any(), any(RemotingCommand.class), anyLong())).thenReturn(responseCommand); + + Future future = rpcClient.invoke(mq, request, defaultTimeout); + RpcResponse actual = future.get(); + + assertEquals(ResponseCode.SUCCESS, actual.getCode()); + assertEquals("1000", new String((byte[]) actual.getBody())); + } + + @Test + public void testInvoke_UPDATE_CONSUMER_OFFSET() throws Exception { + when(request.getCode()).thenReturn(RequestCode.UPDATE_CONSUMER_OFFSET); + + RemotingCommand responseCommand = mock(RemotingCommand.class); + when(responseCommand.getBody()).thenReturn("success".getBytes()); + UpdateConsumerOffsetResponseHeader responseHeader = mock(UpdateConsumerOffsetResponseHeader.class); + when(responseCommand.decodeCommandCustomHeader(UpdateConsumerOffsetResponseHeader.class)).thenReturn(responseHeader); + when(remotingClient.invokeSync(any(), any(RemotingCommand.class), anyLong())).thenReturn(responseCommand); + + Future future = rpcClient.invoke(mq, request, defaultTimeout); + RpcResponse actual = future.get(); + + assertEquals(ResponseCode.SUCCESS, actual.getCode()); + assertEquals("success", new String((byte[]) actual.getBody())); + } + + @Test + public void testInvoke_GET_TOPIC_STATS_INFO() throws Exception { + when(request.getCode()).thenReturn(RequestCode.GET_TOPIC_STATS_INFO); + + RemotingCommand responseCommand = mock(RemotingCommand.class); + TopicStatsTable topicStatsTable = new TopicStatsTable(); + when(responseCommand.getBody()).thenReturn(topicStatsTable.encode()); + when(remotingClient.invokeSync(any(), any(RemotingCommand.class), anyLong())).thenReturn(responseCommand); + + Future future = rpcClient.invoke(mq, request, defaultTimeout); + RpcResponse actual = future.get(); + + assertEquals(ResponseCode.SUCCESS, actual.getCode()); + assertTrue(actual.getBody() instanceof TopicStatsTable); + } + + @Test + public void testInvoke_GET_TOPIC_CONFIG() throws Exception { + when(request.getCode()).thenReturn(RequestCode.GET_TOPIC_CONFIG); + + RemotingCommand responseCommand = mock(RemotingCommand.class); + TopicConfigAndQueueMapping topicConfigAndQueueMapping = new TopicConfigAndQueueMapping(); + when(responseCommand.getBody()).thenReturn(RemotingSerializable.encode(topicConfigAndQueueMapping)); + when(remotingClient.invokeSync(any(), any(RemotingCommand.class), anyLong())).thenReturn(responseCommand); + + Future future = rpcClient.invoke(mq, request, defaultTimeout); + RpcResponse actual = future.get(); + + assertEquals(ResponseCode.SUCCESS, actual.getCode()); + assertTrue(actual.getBody() instanceof TopicConfigAndQueueMapping); + } +} 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 0000000..7804784 --- /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/certs/badClient.key b/remoting/src/test/resources/certs/badClient.key new file mode 100644 index 0000000..2dfd7ab --- /dev/null +++ b/remoting/src/test/resources/certs/badClient.key @@ -0,0 +1,17 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICoTAbBgkqhkiG9w0BBQMwDgQIc2h7vaLYK6gCAggABIICgNrkvD1Xxez79Jgk +WhRJg06CG8UthncfeuymR4hgp9HIneUzUHOoaf64mpxUbDWe3YOzA29REcBQsjF0 +Rpv+Uyg3cyDG14TmeRoSufOxB3MWLcIenoPPyNNtxe3XXmdkJTXX2YR0j7EOzH2v +qlmuxmN4A7UonV5RdGxCz0sm7bU7EyZKdLO/DwBNxlX7ukcVLxAAqsc7ondclYj0 +SFJKk1nzfysCsk/Pq+q3PAVVpG6x5RFaLVS7Zt+gU6IEp+0S0eeYukkTjGh9PMPl +wjCOcRiR3O+g4b3DevmW8TcoBqAZ2cFaf4lGhYlNBfa9PaQ3spJLL8l8xBbRIs8T +3UnaFIa49r9DO/ZpCwpDeUE+URCx/SpcO6lchWQhdEuFt+DnFKOPYDSCHtHJSWHf +9Z2bltjcYYPy/8nkPeqsO9vn4/r6jo+l7MYWKyWolLCW+7RYbpx5R2s4SBGtBP6w +bwQOtOASbpG+mqTf7+ARpffHaZm9cKoKwobXigjDojPeaBCg5DgRuLIS1tO46Pjg +USJ8sZilXifUwc6qRZ/2KiTSiJYCPMJD2ZTvK2Inkv2qzg6X3kw7CYCaW+iDL9zN +e3ES7bps1wZ6D8cGq80WUQgrtpaGAXLzIv4FvM5yDoqrre/dh/XDO9l2hYfUmRVv +rynKdSxjhhyHaK2ei8cX4LGEIlRNiu9ZIxSYeUAy37IJ0rVC7vtBWTh30JTeMRop +iIPmygBMX2FEhQ2l/eS2lRhiybR0QXA4kCeJkVQas3aMMBGp2ThPNahLpzP82B7V +f9137okQC95/KXRz/ZLYFsJtY/53206mG7gU/+dYsYI4slLAlnSe8k2sS0D9qkWJ +VV9F7PM= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/remoting/src/test/resources/certs/badClient.pem b/remoting/src/test/resources/certs/badClient.pem new file mode 100644 index 0000000..1d264d3 --- /dev/null +++ b/remoting/src/test/resources/certs/badClient.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC8zCCAdsCAQIwDQYJKoZIhvcNAQEFBQAwfDELMAkGA1UEBhMCemgxCzAJBgNV +BAgMAnpqMQswCQYDVQQHDAJoejEPMA0GA1UECgwGYXBhY2hlMREwDwYDVQQLDAhy +b2NrZXRtcTEOMAwGA1UEAwwFeXVrb24xHzAdBgkqhkiG9w0BCQEWEHl1a29uQGFw +YWNoZS5vcmcwHhcNMTcxMjExMDk0NDExWhcNMTgwMTEwMDk0NDExWjCBhjELMAkG +A1UEBhMCemgxCzAJBgNVBAgMAnpqMQswCQYDVQQHDAJoejEPMA0GA1UECgwGYXBh +Y2hlMREwDwYDVQQLDAhyb2NrZXRtcTEWMBQGA1UEAwwNZm9vYmFyLmNsaWVudDEh +MB8GCSqGSIb3DQEJARYSZm9vQGJhci5jbGllbnQuY29tMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQC+3bvrKGF1Y9/kN5UBtf8bXRtxn6L1W6mCRrX6aHBb+vQp +BEYk3Pwu/OLd7TkOC5zwjCIPIlwV4FaYnWh0KooqpmvXuKJLAQBFa8yGWERYys73 +9a/U31cu6lndnG2lZfb47NTy+KdzDYsqB4GfnASqA7PbxJHDU4Fu7wp7gN3HRQID +AQABMA0GCSqGSIb3DQEBBQUAA4IBAQBsFroSKr3MbCq1HjWpCLDEz2uS4LQV6L1G +smNfGNY17ELOcY9uweBBXOsfKVOEizYJJqatbJlz6FmPkIbfsGW2Wospkp1gvYMy +NGL27vX3rB5vOo5vdFITaaV9/dEu53A0iWdsn3wH/FJnMsqBmynb+/3FY+Lff9d1 +XBaXLr+DeBx4lrE8rWTvhWh8gqDkuNLBTygdH0+g8/xkqhQhLqjIlMCSnrG2cTfj +LewizVcX/VZ6DNC2M2vjEFbCShclZHocG80N7udl5KNsLEU2jyO1F61Q0yo+VYGS +7n8dRYgbOKyCjMdu69fAfZvp4aoy1SXqtjMphDh5R7y7mhP60e0A +-----END CERTIFICATE----- diff --git a/remoting/src/test/resources/certs/badServer.key b/remoting/src/test/resources/certs/badServer.key new file mode 100644 index 0000000..de4f7c5 --- /dev/null +++ b/remoting/src/test/resources/certs/badServer.key @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALBvxESq2VvSpJl1 +skv8SzyPYKgU8bZx37hEOCmoeYvd9gWNfeYZuITng2/5mpWX+zuAgKsgPU66YG0v +++dT5GBQPr0Imb25IMl3xOY2eEiLeMokYiWbnA1C+pw1a27zMqk6pgbcRaMfLdh5 +npusWtqBzZIxqo1TpaOGEmyQTNRlAgMBAAECgYBSigbUZOTIWxObov7lI0MDMsPx +/dJSGpWhe3CWtHUgJJdKY7XpJlE3A6Nuh+N0ZiQm4ufOpodnxDMGAXOj9ZAZY16Y +i7I0ayXepcpTqYqo0o0+ze2x7SECAXe26bqvLRuKG2hpUyM59vAmll9gmQM5n8z4 +ZzoAzqRqkRHdo5bTxQJBAOF6SwSSfb8KEtTjWpJ48W1PO/NmKbW3QsNCWuk/w5p7 +E8L2g3nwakJiFmVNCga74rUbcgbCkw7y/lLeM8yC74MCQQDIUgCN/vuHm+eT85xk +QoVKhDljXzLoog6wTUf5SMtrmUFTbQqyvw5xjHYdp3TWJM/Px8IyLxOr97sSnnft +l7/3AkEAukYLv6U+GRs7X4DMDIG6AjIZNwXJo4PYtfMVo+i3seHH+6MoDw8c2eaq +1dmFVPbXXgNkek04rHr2vIMxi90H/QJAAMOfUOtaFkhX986EGDXQwFoExgZE8XI8 +0BtbXO4UKJLrFuBhnBDygyhgAvjyjyaQzGAcs4hOcOd/BTEpj/R2PQJBANUKa9T9 +qWYhDhWN1Uj7qXhC1j2z/vTAzcYuwhpPRjt3RaVl27itI7cqiGquFhwfKZZFaOh5 +pnnWHv63YbGQ2Qc= +-----END PRIVATE KEY----- diff --git a/remoting/src/test/resources/certs/badServer.pem b/remoting/src/test/resources/certs/badServer.pem new file mode 100644 index 0000000..ada0a94 --- /dev/null +++ b/remoting/src/test/resources/certs/badServer.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC5DCCAcwCAQEwDQYJKoZIhvcNAQEFBQAwfDELMAkGA1UEBhMCemgxCzAJBgNV +BAgMAnpqMQswCQYDVQQHDAJoejEPMA0GA1UECgwGYXBhY2hlMREwDwYDVQQLDAhy +b2NrZXRtcTEOMAwGA1UEAwwFeXVrb24xHzAdBgkqhkiG9w0BCQEWEHl1a29uQGFw +YWNoZS5vcmcwHhcNMTcxMjExMDk0MzE3WhcNMTgwMTEwMDk0MzE3WjB4MQswCQYD +VQQGEwJ6aDELMAkGA1UECAwCemoxCzAJBgNVBAcMAmh6MQ8wDQYDVQQKDAZhcGFj +aGUxETAPBgNVBAsMCHJvY2tldG1xMQ8wDQYDVQQDDAZmb29iYXIxGjAYBgkqhkiG +9w0BCQEWC2Zvb0BiYXIuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCw +b8REqtlb0qSZdbJL/Es8j2CoFPG2cd+4RDgpqHmL3fYFjX3mGbiE54Nv+ZqVl/s7 +gICrID1OumBtL/vnU+RgUD69CJm9uSDJd8TmNnhIi3jKJGIlm5wNQvqcNWtu8zKp +OqYG3EWjHy3YeZ6brFragc2SMaqNU6WjhhJskEzUZQIDAQABMA0GCSqGSIb3DQEB +BQUAA4IBAQAx+0Se3yIvUOe23oQp6UecaHtfXJCZmi1p5WbwJi7jUcYz78JB8oBj +tVsa+1jftJG+cJJxqgxo2IeIAVbcEteO19xm7dc8tgfH/Bl0rxQz4WEYKb2oF/EQ +eRgcvj4uZ0d9WuprAvJgA4r0Slu2ZZ0cVkzi06NevTweTBYIKFzHaPShqUWEw8ki +42V5jAtRve7sT0c4TH/01dd2fs3V4Ul3E2U3LOP6VizIfKckdht0Bh6B6/5L8wvH +4l1f4ni7w34vXGANpmTP2FGjQQ3kYjKL7GzgMphh3Kozhil6g1GLMhxvp6ccCA9W +m5g0cPa3RZnjI/FoD0lZ5S1Q5s9qXbLm +-----END CERTIFICATE----- diff --git a/remoting/src/test/resources/certs/ca.pem b/remoting/src/test/resources/certs/ca.pem new file mode 100644 index 0000000..d33cb49 --- /dev/null +++ b/remoting/src/test/resources/certs/ca.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDyzCCArOgAwIBAgIJAKzXC2VLdPclMA0GCSqGSIb3DQEBBQUAMHwxCzAJBgNV +BAYTAnpoMQswCQYDVQQIDAJ6ajELMAkGA1UEBwwCaHoxDzANBgNVBAoMBmFwYWNo +ZTERMA8GA1UECwwIcm9ja2V0bXExDjAMBgNVBAMMBXl1a29uMR8wHQYJKoZIhvcN +AQkBFhB5dWtvbkBhcGFjaGUub3JnMB4XDTE3MTIxMTA5MjUxNFoXDTE4MDExMDA5 +MjUxNFowfDELMAkGA1UEBhMCemgxCzAJBgNVBAgMAnpqMQswCQYDVQQHDAJoejEP +MA0GA1UECgwGYXBhY2hlMREwDwYDVQQLDAhyb2NrZXRtcTEOMAwGA1UEAwwFeXVr +b24xHzAdBgkqhkiG9w0BCQEWEHl1a29uQGFwYWNoZS5vcmcwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDLUZi/zPj+7sYbfTng/gJeHpvvrWZkiudNwh1t +5kxAusrJyGBkGm+xmRPJeQPZzbhfwfrz/UiQSbjlyV4K+SEZuNIHBSU80aTnXFWg +wIgIAKvu3ZwYkcTjSDBvZv1DgbRkuqAB5ExsJ4vovoNqZcsLFLKsqT1G7lTAwRKU +/FTKgD4g/zvhEoolonzKuk7CPivfKWFzcTpe8zRQlI0O9+j9Pq38F+5yxP7atK/b +uYw36Efgt8nbkjusWIyXibpDMbAUroJNNYlFnunb+XKLpslkrIrfLGiMUq2Ru940 +ooQaANYWzogRQeIofsMN6H9CCRXtVIzcgJJU3wWXGXPRuNr7AgMBAAGjUDBOMB0G +A1UdDgQWBBTd3bmAcazOY2/TI/h4zaGhni+nJzAfBgNVHSMEGDAWgBTd3bmAcazO +Y2/TI/h4zaGhni+nJzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBp +KRcnYsVtFZJejyt02+7SaMudTNRgh5SexFWsl1O7qWUc+fMVgMHHDzGRbkdevcdZ +9uKDwUoa6M1wlOeosTTfQlH9b/AwW6QT7KqdpcpMXlmoV/PNCAVt2QeVclmplvqo +Rx8qUHNckvvzNZt1W6AkBG93P0BLK/3FMJDyYmxkstwnpBPf/3A+t5k2INUI7yQf +B3Tqzs/4iQ3idCLqz2WhTNUYpZOREtpJMcFaOdMsGNnIF+LvkKGij0MPVd/mwJtL +UvQXwbOWpCS7A73sWFqPnrSzpi4VwcvAsi8lUYXsc0H064oagb58zvYz3kXqybcb +KQntj5dP4C3lLHUTTcAV +-----END CERTIFICATE----- diff --git a/remoting/src/test/resources/certs/client.key b/remoting/src/test/resources/certs/client.key new file mode 100644 index 0000000..c30daea --- /dev/null +++ b/remoting/src/test/resources/certs/client.key @@ -0,0 +1,17 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICoTAbBgkqhkiG9w0BBQMwDgQI1vtPpDhOYRcCAggABIICgMHwgw0p9fx95R/+ +cWnNdEq8I3ZOOy2wDjammFvPrYXcCJzS3Xg/0GDJ8pdJRKrI7253e4u3mxf5oMuY +RrvpB3KfdelU1k/5QKqOxL/N0gQafQLViN53f6JelyBEAmO1UxQtKZtkTrdZg8ZP +0u1cPPWxmgNdn1Xx3taMw+Wo05ysHjnHJhOEDQ2WT3VXigiRmFSX3H567yjYMRD+ +zmvBq+qqR9JPbH9Cn7X1oRXX6c8VsZHWF/Ds0I4i+5zJxsSIuNZxjZw9XXNgXtFv +7FEFC0HDgDQQUY/FNPUbmjQUp1y0YxoOBjlyIqBIx5FWxu95p2xITS0OimQPFT0o +IngaSb+EKRDhqpLxxIVEbDdkQrdRqcmmLGJioAysExTBDsDwkaEJGOp44bLDM4QW +SIA9SB01omuCXgn7RjUyVXb5g0Lz+Nvsfp1YXUkPDO9hILfz3eMHDSW7/FzbB81M +r8URaTagQxBZnvIoCoWszLDXn3JwEjpZEA6y55Naptps3mMRf7+XMt42lX0e4y9a +ogNu5Zw/RZD9YcaTjC2z5XeKiMCs1Ymhy9iuzbo+eRGESqzvUE4VirtsiEwxJRci +JHAvuAl3X4XnpTty4ahOU+DihM9lALxdU68CN9++7mx581pYuvjzrV+Z5+PuptZX +AjCZmkZLDh8TCHSzWRqvP/Hcvo9BjW8l1Lq6tOa222PefSNCc6gs6Hq+jUghbabZ +/ux4WuFc0Zd6bfQWAZohSvd78/ixsdJPNGm2OP+LUIrEDKIkLuH1PM0uq4wzJZNu +Bo7oJ5iFWF67u3MC8oq+BqOVKDNWaCMi7iiSrN2XW8FBo/rpx4Lf/VYREL+Y0mP6 +vzJrZqw= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/remoting/src/test/resources/certs/client.pem b/remoting/src/test/resources/certs/client.pem new file mode 100644 index 0000000..cb6580d --- /dev/null +++ b/remoting/src/test/resources/certs/client.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDATCCAekCAQIwDQYJKoZIhvcNAQEFBQAwfDELMAkGA1UEBhMCemgxCzAJBgNV +BAgMAnpqMQswCQYDVQQHDAJoejEPMA0GA1UECgwGYXBhY2hlMREwDwYDVQQLDAhy +b2NrZXRtcTEOMAwGA1UEAwwFeXVrb24xHzAdBgkqhkiG9w0BCQEWEHl1a29uQGFw +YWNoZS5vcmcwIBcNMTgwMTE2MDYxNjQ0WhgPMjExNzEyMjMwNjE2NDRaMIGSMQsw +CQYDVQQGEwJDTjERMA8GA1UECAwIWmhlamlhbmcxETAPBgNVBAcMCEhhbmd6aG91 +MQ8wDQYDVQQKDAZhcGFjaGUxETAPBgNVBAsMCHJvY2tldG1xMRgwFgYDVQQDDA9h +cGFjaGUgcm9ja2V0bXExHzAdBgkqhkiG9w0BCQEWEHl1a29uQGFwYWNoZS5vcmcw +gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOjPlSjZk37XLBJBc5G/qQNsNdVD +vZnEGntrqW0UuHjF2T/LPtsGOavLP5wCHvn2zwMR2eCXZwKdKIzSvk0L3XOjH/XY +OLgRa3cg90lV7Wzn9UMGq3nOjFtjIODPjtz3lwYAuAt1MH+K0E+ChuCFBgFqdY9U +E0suW3DX0Mt/WB3pAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAFGPaZKyCZzQihKj +n/7I1J0wKl1HrU7N4sOie8E+ntcpKeX9zKYAou/4Iy0qwgxgRsnucB1rDous560a ++8DFDU8+FnikK9cQtKfQqu4F266IkkXolviZMSfkmB+NIsByIl95eMJlQHVlAvnX +vnpGdhD/Jhs+acE1VHhO6K+8omKLA6Og8MmYGRwmnBLcxIvqoSNDlEShfQyjaECg +I4bEi4ZhH3lSHE46FybJdoxDbj9IjHWqpOnjM23EOyfd1zcwOZJA7a54kfOpiTjz +wrtes5yoQznun5WtGcLM8ZmyaQ+Jr3j6NyZhOwULzK1+A8YUsW6Ww39xTxQoIHEQ +7eirb54= +-----END CERTIFICATE----- diff --git a/remoting/src/test/resources/certs/privkey.pem b/remoting/src/test/resources/certs/privkey.pem new file mode 100644 index 0000000..6955568 --- /dev/null +++ b/remoting/src/test/resources/certs/privkey.pem @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIBTqUKpwFlcUCAggA +MBQGCCqGSIb3DQMHBAii5M3Oni0WEwSCBMj5amhdPBva0QxgsWWrSBkfvmc3QQbl +YQE5gHXzL5oaG0lbXCLQe19pr9jFckdDH8uPIi+aRie3WpZXzYLdihjsV0CgE7iy +90ac9fgzIZbJIk/WQIDgwUZm5dEYo2v+B0WwKiD5IHzlTlXM6HVv+DRdPwKjHjAt +TCcCSPUgjKVxHWFtQzY7mqo8P8wcNQHGkEfoQQub6tEsUDeesjS0FoK5Z2oYsmhW +d0PNuGXw3UIMbG109DmC2ILFuTf5WSc7mxI11qL9Z5wTmcFqN7KKb8+MIQEoteni +HICOFfKQWn8er14lmYw9anQAyaeyF/JnYkmVB8vaHBFYs/5EFZtvznpEIIhLKCuG +lve8PJQmfWuBlPdwwJhCXHrLvjfwku4jUF8febU0BHZ5HETaB195g8r9RWfdZcrG +f3fMO4Kq/YoP6oSxKhMP4L2pwj57EMV9N5P87ZyDFNp9BwgIjCawDRUc1Gi9YKak +rpDNabTCr0I3NW27VGGF9m/mby7BLragc01LgTH7SFWS+1D/61/V2YBDFmWn2yV4 +4eKGeBkR3w0m/nWWfNXko8UzM/hjJ4P7Njq8HXdvEpnbDRZWzwdTGWTEvn/TAI3h +j7vmWUHdpOQgb0WGlvEUx3V9wi2Fc1rCseHtYZgLf3KdKYHauPAMSON7KBtKaFuU +6685sUoJbhahN7ILfP3sDxM3VYjSvlPL00lgOdqDT/iO6pNXvnNnQROCCE4kcOQT +uSnEu+wmFHj+QlC60ftRl6zGVqjBxf1+TGmzTEByAOfZtEQ8V/clzRI4BCxYbCAG +mJSa+q1RSju8yClBkXGT2zfhUeNqJnXEIaD/uXCPVGg7hfLyCcVVSmL97aw9QAIe +lBJN/4bdxXLnJaHFKyztRe9N97JAKY9HAPMKKhKtqprWB7LedTIPHtXnpoSjyTrG +SEtlOTQ38s3v9bUPXqF7TYZb+ytj5bIQpl6+WqF9ZNj3gRyx7rcsILhBBg08olVQ +WZwr7LlIxUcDlrbYmrwd9lsMz2nOW2CLCD7mVqJQa2Wm35l6vJHQAI0WiQlHnopC +M2Y49JruWWim2lC8ZzHgTyiU54bIfkXKQxua8G5WGxpW4dDRrM1d0uYe9M1TOqvP +jFxq+XEIj/LntJpY7XIZs+33wuNLIVvkee9zsap++zYNH+KIGmbXz/HUO6gYeJYw +EeaBTLfXtNlgHV9TpMjj3Js6p8hMoVDx27kzPOXa3nrFbHiHmUYY39ZSBCE53Wew +SKIr/FlKtthzJwJkoMNxxsZ1QcI6WgmRANSC4oP9OyM+hPnWlISJZsy9UlXRZNEJ +1lI8P/FUbdk/hsRN98j4pb/hzI0yyG1tKaj0TBipjdEsxfthKdwS2sE1wG+F2hrg +A1jG+UOYmreQjCbrzEiq0J0H4wSL6/s/zN7SyXIWBG0UFalWFiehG5242mEOyX03 +0Yi94mPhF/kcqYsWZ/JJo6cq/EqgIeIqEzkbKx+TOsXk13K2y6apgvCxeHDDv3yT +DqQueRgFicl0319yEK8ARnREFBm8D5oqwMHjJzVzjrqhFGLW1jfQG9HEW5A+s8WF +d+2OtH1o/jVdAPXoP1DnxdF+G7fNXDI4cyjejC7uhLuxHCOx648UpRE9+mCiI2IO +LDM= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/remoting/src/test/resources/certs/server.key b/remoting/src/test/resources/certs/server.key new file mode 100644 index 0000000..30df696 --- /dev/null +++ b/remoting/src/test/resources/certs/server.key @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOsmp4YtrIRsBdBQ +LyPImafCRynTJls3NNF4g6nZr9e0efBY830gw9kBebcm603sdZNl95fzRr2+srXi +5FJbG7Fmq1+F0xLNK/kKWirGtNMT2DubmhVdKyXYJSvInoGRkrQzbOG0MdAyzE6Q +O6OjjNN+xGkmadWyCyNF6S8YqMJTAgMBAAECgYEAj0OlnOIG0Ube4+N2VN7KfqKm +qJy0Ka6gx14dGUY/E7Qo9n27GujzaSq09RkJExiVKZBeIH1fBAtC5f2uDV7kpy0l +uNpTpQkbw0g2EQLxDsVwaUEYbu+t9qVeXoDd1vFeoXHBuRwvI9UW1BrxVtvKODia +5StU8Lw4yjcm2lQalwECQQD/sKj56thIsIY7D9qBHk7fnFLd8aYzhnP2GsbZX4V/ +T1KHRxr/8MqdNQX53DE5qcyM/Mqu95FIpTAniUtvcBujAkEA62+fAMYFTAEWj4Z4 +vCmcoPqfVPWhBKFR/wo3L8uUARiIzlbYNU3LIqC2s16QO50+bLUd41oVHNw9Y+uM +fxQpkQJACg/WpncSadHghmR6UchyjCQnsqo2wyJQX+fv2VAD/d2OPtqSem3sW0Fh +6dI7cax36zhrdXUyl2xAt92URV9hBwJALX93sdWSxnpbWsc449wCydVFH00MnfFz +AB+ARLtJ0eBk58M+qyZqgDmgtQ8sPmkH3EgwC3SoKdiiAIJPt2s1EQJBAKnISZZr +qB2F2PfAW2JJbQlrPyVzkxhv9XYdiVNOErmuxLFae3AI7nECgGuFBtvmeqzm2yRj +7RBMCmzyWG7MF3o= +-----END PRIVATE KEY----- diff --git a/remoting/src/test/resources/certs/server.pem b/remoting/src/test/resources/certs/server.pem new file mode 100644 index 0000000..0187247 --- /dev/null +++ b/remoting/src/test/resources/certs/server.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDATCCAekCAQIwDQYJKoZIhvcNAQEFBQAwfDELMAkGA1UEBhMCemgxCzAJBgNV +BAgMAnpqMQswCQYDVQQHDAJoejEPMA0GA1UECgwGYXBhY2hlMREwDwYDVQQLDAhy +b2NrZXRtcTEOMAwGA1UEAwwFeXVrb24xHzAdBgkqhkiG9w0BCQEWEHl1a29uQGFw +YWNoZS5vcmcwIBcNMTgwMTE2MDYxMzQ5WhgPMjExNzEyMjMwNjEzNDlaMIGSMQsw +CQYDVQQGEwJDTjERMA8GA1UECAwIWmhlamlhbmcxETAPBgNVBAcMCEhhbmd6aG91 +MQ8wDQYDVQQKDAZhcGFjaGUxETAPBgNVBAsMCHJvY2tldG1xMRgwFgYDVQQDDA9h +cGFjaGUgcm9ja2V0bXExHzAdBgkqhkiG9w0BCQEWEHl1a29uQGFwYWNoZS5vcmcw +gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOsmp4YtrIRsBdBQLyPImafCRynT +Jls3NNF4g6nZr9e0efBY830gw9kBebcm603sdZNl95fzRr2+srXi5FJbG7Fmq1+F +0xLNK/kKWirGtNMT2DubmhVdKyXYJSvInoGRkrQzbOG0MdAyzE6QO6OjjNN+xGkm +adWyCyNF6S8YqMJTAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAAzbwXyAULmXitiU ++8/2vbUZQlzB/nXY52OIq7qu3F55hE5qlHkcVxG2JZjO3p5UETwOyNUpU4dpu3uT +7WSdygH4Iagl87ILpGsob9pAf0joAbaXAY4sGDhg+WjR5JInAxbmT+QWZ+4NTuLQ +fSudUSJrv+HmUlmcVOvLiNStgt9rbtcgJAvpVwY+iCv0HQziFuQxmOkDv09ZLzu/ +lxCMqnbgkEFYkwdntN6MVk38K3MovszedGO/n19hNOFss7nn5XDEeEnc6BqKGdck +YDoy6amohY0Ds0o0gJ2rq0Y8Gjl9spQ3oeXpoNUoz84OF4KIBRTzSMv8CrmqPdFY +Zd2MGjw= +-----END CERTIFICATE----- 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 0000000..8695d52 --- /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 0000000..a47a60c --- /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 new file mode 100644 index 0000000..bbf1ea9 --- /dev/null +++ b/srvutil/pom.xml @@ -0,0 +1,56 @@ + + + + + org.apache.rocketmq + rocketmq-all + 5.3.4-SNAPSHOT + + + 4.0.0 + jar + rocketmq-srvutil + rocketmq-srvutil ${project.version} + + + ${basedir}/.. + + + + + ${project.groupId} + rocketmq-remoting + + + ${project.groupId} + rocketmq-common + + + commons-cli + commons-cli + + + com.google.guava + guava + + + com.googlecode.concurrentlinkedhashmap + concurrentlinkedhashmap-lru + + + 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 0000000..0c0690d --- /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(entry.getValue())) { + 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 new file mode 100644 index 0000000..5a8a7cd --- /dev/null +++ b/srvutil/src/main/java/org/apache/rocketmq/srvutil/ServerUtil.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.srvutil; + +import java.util.Properties; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +public class ServerUtil { + + public static Options buildCommandlineOptions(final Options options) { + Option opt = new Option("h", "help", false, "Print help"); + opt.setRequired(false); + options.addOption(opt); + + opt = + new Option("n", "namesrvAddr", true, + "Name server address list, eg: '192.168.0.1:9876;192.168.0.2:9876'"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + public static CommandLine parseCmdLine(final String appName, String[] args, Options options, + CommandLineParser parser) { + HelpFormatter hf = new HelpFormatter(); + hf.setWidth(110); + CommandLine commandLine = null; + try { + commandLine = parser.parse(options, args); + if (commandLine.hasOption('h')) { + hf.printHelp(appName, options, true); + System.exit(0); + } + } catch (ParseException e) { + System.err.println(e.getMessage()); + hf.printHelp(appName, options, true); + System.exit(1); + } + + return commandLine; + } + + public static void printCommandLineHelp(final String appName, final Options options) { + HelpFormatter hf = new HelpFormatter(); + hf.setWidth(110); + hf.printHelp(appName, options, true); + } + + public static Properties commandLine2Properties(final CommandLine commandLine) { + Properties properties = new Properties(); + Option[] opts = commandLine.getOptions(); + + if (opts != null) { + for (Option opt : opts) { + String name = opt.getLongOpt(); + String value = commandLine.getOptionValue(name); + if (value != null) { + properties.setProperty(name, value); + } + } + } + + return properties; + } + +} diff --git a/srvutil/src/main/java/org/apache/rocketmq/srvutil/ShutdownHookThread.java b/srvutil/src/main/java/org/apache/rocketmq/srvutil/ShutdownHookThread.java new file mode 100644 index 0000000..15b57bf --- /dev/null +++ b/srvutil/src/main/java/org/apache/rocketmq/srvutil/ShutdownHookThread.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.srvutil; + +import org.apache.rocketmq.logging.org.slf4j.Logger; + +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * {@link ShutdownHookThread} is the standard hook for filtersrv and namesrv modules. + * Through {@link Callable} interface, this hook can customization operations in anywhere. + */ +public class ShutdownHookThread extends Thread { + private volatile boolean hasShutdown = false; + private AtomicInteger shutdownTimes = new AtomicInteger(0); + private final Logger log; + private final Callable callback; + + /** + * Create the standard hook thread, with a call back, by using {@link Callable} interface. + * + * @param log The log instance is used in hook thread. + * @param callback The call back function. + */ + public ShutdownHookThread(Logger log, Callable callback) { + super("ShutdownHook"); + this.log = log; + this.callback = callback; + } + + /** + * Thread run method. + * Invoke when the jvm shutdown. + * 1. count the invocation times. + * 2. execute the {@link ShutdownHookThread#callback}, and time it. + */ + @Override + public void run() { + synchronized (this) { + log.info("shutdown hook was invoked, " + this.shutdownTimes.incrementAndGet() + " times."); + if (!this.hasShutdown) { + this.hasShutdown = true; + long beginTime = System.currentTimeMillis(); + try { + this.callback.call(); + } catch (Exception e) { + log.error("shutdown hook callback invoked failure.", e); + } + long consumingTimeTotal = System.currentTimeMillis() - beginTime; + log.info("shutdown hook done, consuming time total(ms): " + consumingTimeTotal); + } + } + } +} 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 0000000..aeae625 --- /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 0000000..8695d52 --- /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 0000000..269ff2d --- /dev/null +++ b/store/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 = "store", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//common", + "//remoting", + "@maven//:com_alibaba_fastjson", + "@maven//:com_alibaba_fastjson2_fastjson2", + "@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", + "@maven//:com_google_code_findbugs_jsr305", + "@maven//:commons_validator_commons_validator", + ], +) + +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", + "@maven//:org_junit_jupiter_junit_jupiter_api", + ], +) + +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 new file mode 100644 index 0000000..b3f8ee8 --- /dev/null +++ b/store/pom.xml @@ -0,0 +1,75 @@ + + + + + org.apache.rocketmq + rocketmq-all + 5.3.4-SNAPSHOT + + + 4.0.0 + jar + rocketmq-store + rocketmq-store ${project.version} + + + ${basedir}/.. + + + + + io.openmessaging.storage + dledger + + + org.apache.rocketmq + rocketmq-remoting + + + + + ${project.groupId} + rocketmq-remoting + + + net.java.dev.jna + jna + + + com.conversantmedia + disruptor + + + com.google.guava + guava + + + commons-io + commons-io + + + + org.slf4j + slf4j-api + + + 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 new file mode 100644 index 0000000..d9cd602 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/AllocateMappedFileService.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.store; + +import java.io.File; +import java.io.IOException; +import java.util.ServiceLoader; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.TimeUnit; +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.apache.rocketmq.store.logfile.DefaultMappedFile; +import org.apache.rocketmq.store.logfile.MappedFile; + +/** + * Create MappedFile in advance + */ +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<>(); + private PriorityBlockingQueue requestQueue = + new PriorityBlockingQueue<>(); + private volatile boolean hasException = false; + private DefaultMessageStore messageStore; + + public AllocateMappedFileService(DefaultMessageStore messageStore) { + this.messageStore = messageStore; + } + + public MappedFile putRequestAndReturnMappedFile(String nextFilePath, String nextNextFilePath, int fileSize) { + int canSubmitRequests = 2; + 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.remainTransientStoreBufferNumbs() - this.requestQueue.size(); + } + } + + AllocateRequest nextReq = new AllocateRequest(nextFilePath, fileSize); + boolean nextPutOK = this.requestTable.putIfAbsent(nextFilePath, nextReq) == null; + + if (nextPutOK) { + if (canSubmitRequests <= 0) { + log.warn("[NOTIFYME]TransientStorePool is not enough, so create mapped file error, " + + "RequestQueueSize : {}, StorePoolSize: {}", this.requestQueue.size(), this.messageStore.remainTransientStoreBufferNumbs()); + this.requestTable.remove(nextFilePath); + return null; + } + boolean offerOK = this.requestQueue.offer(nextReq); + if (!offerOK) { + log.warn("never expected here, add a request to preallocate queue failed"); + } + canSubmitRequests--; + } + + AllocateRequest nextNextReq = new AllocateRequest(nextNextFilePath, fileSize); + boolean nextNextPutOK = this.requestTable.putIfAbsent(nextNextFilePath, nextNextReq) == null; + if (nextNextPutOK) { + if (canSubmitRequests <= 0) { + log.warn("[NOTIFYME]TransientStorePool is not enough, so skip preallocate mapped file, " + + "RequestQueueSize : {}, StorePoolSize: {}", this.requestQueue.size(), this.messageStore.remainTransientStoreBufferNumbs()); + this.requestTable.remove(nextNextFilePath); + } else { + boolean offerOK = this.requestQueue.offer(nextNextReq); + if (!offerOK) { + log.warn("never expected here, add a request to preallocate queue failed"); + } + } + } + + if (hasException) { + log.warn(this.getServiceName() + " service has exception. so return null"); + return null; + } + + 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; + } else { + this.requestTable.remove(nextFilePath); + return result.getMappedFile(); + } + } else { + log.error("find preallocate mmap failed, this never happen"); + } + } catch (InterruptedException e) { + log.warn(this.getServiceName() + " service has exception. ", e); + } + + return null; + } + + @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() { + super.shutdown(true); + for (AllocateRequest req : this.requestTable.values()) { + if (req.mappedFile != null) { + log.info("delete pre allocated mapped file, {}", req.mappedFile.getFileName()); + req.mappedFile.destroy(1000); + } + } + } + + @Override + public void run() { + log.info(this.getServiceName() + " service started"); + + while (!this.isStopped() && this.mmapOperation()) { + + } + log.info(this.getServiceName() + " service end"); + } + + /** + * Only interrupted by the external thread, will return false + */ + private boolean mmapOperation() { + boolean isSuccess = false; + AllocateRequest req = null; + try { + req = this.requestQueue.take(); + AllocateRequest expectedRequest = this.requestTable.get(req.getFilePath()); + if (null == expectedRequest) { + log.warn("this mmap request expired, maybe cause timeout " + req.getFilePath() + " " + + req.getFileSize()); + return true; + } + if (expectedRequest != req) { + log.warn("never expected here, maybe cause timeout " + req.getFilePath() + " " + + req.getFileSize() + ", req:" + req + ", expectedRequest:" + expectedRequest); + return true; + } + + if (req.getMappedFile() == null) { + long beginTime = System.currentTimeMillis(); + + MappedFile mappedFile; + 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 DefaultMappedFile(req.getFilePath(), req.getFileSize(), messageStore.getTransientStorePool()); + } + } else { + mappedFile = new DefaultMappedFile(req.getFilePath(), req.getFileSize()); + } + + long elapsedTime = UtilAll.computeElapsedTimeMilliseconds(beginTime); + if (elapsedTime > 10) { + int queueSize = this.requestQueue.size(); + log.warn("create mappedFile spent time(ms) " + elapsedTime + " queue size " + queueSize + + " " + req.getFilePath() + " " + req.getFileSize()); + } + + // pre write mappedFile + if (mappedFile.getFileSize() >= this.messageStore.getMessageStoreConfig() + .getMappedFileSizeCommitLog() + && + this.messageStore.getMessageStoreConfig().isWarmMapedFileEnable()) { + mappedFile.warmMappedFile(this.messageStore.getMessageStoreConfig().getFlushDiskType(), + this.messageStore.getMessageStoreConfig().getFlushLeastPagesWhenWarmMapedFile()); + } + + req.setMappedFile(mappedFile); + this.hasException = false; + isSuccess = true; + } + } catch (InterruptedException e) { + log.warn(this.getServiceName() + " interrupted, possibly by shutdown."); + this.hasException = true; + return false; + } catch (IOException e) { + log.warn(this.getServiceName() + " service has exception. ", e); + this.hasException = true; + if (null != req) { + requestQueue.offer(req); + try { + Thread.sleep(1); + } catch (InterruptedException ignored) { + } + } + } finally { + if (req != null && isSuccess) + req.getCountDownLatch().countDown(); + } + return true; + } + + static class AllocateRequest implements Comparable { + // Full file path + private String filePath; + private int fileSize; + private CountDownLatch countDownLatch = new CountDownLatch(1); + private volatile MappedFile mappedFile = null; + + public AllocateRequest(String filePath, int fileSize) { + this.filePath = filePath; + this.fileSize = fileSize; + } + + public String getFilePath() { + return filePath; + } + + public void setFilePath(String filePath) { + this.filePath = filePath; + } + + public int getFileSize() { + return fileSize; + } + + public void setFileSize(int fileSize) { + this.fileSize = fileSize; + } + + public CountDownLatch getCountDownLatch() { + return countDownLatch; + } + + public void setCountDownLatch(CountDownLatch countDownLatch) { + this.countDownLatch = countDownLatch; + } + + public MappedFile getMappedFile() { + return mappedFile; + } + + public void setMappedFile(MappedFile mappedFile) { + this.mappedFile = mappedFile; + } + + public int compareTo(AllocateRequest other) { + if (this.fileSize < other.fileSize) + return 1; + else if (this.fileSize > other.fileSize) { + return -1; + } else { + int mIndex = this.filePath.lastIndexOf(File.separator); + long mName = Long.parseLong(this.filePath.substring(mIndex + 1)); + int oIndex = other.filePath.lastIndexOf(File.separator); + long oName = Long.parseLong(other.filePath.substring(oIndex + 1)); + if (mName < oName) { + return -1; + } else if (mName > oName) { + return 1; + } else { + return 0; + } + } + // return this.fileSize < other.fileSize ? 1 : this.fileSize > + // other.fileSize ? -1 : 0; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((filePath == null) ? 0 : filePath.hashCode()); + result = prime * result + fileSize; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + AllocateRequest other = (AllocateRequest) obj; + if (filePath == null) { + if (other.filePath != null) + return false; + } else if (!filePath.equals(other.filePath)) + return false; + if (fileSize != other.fileSize) + return false; + return true; + } + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/AppendMessageCallback.java b/store/src/main/java/org/apache/rocketmq/store/AppendMessageCallback.java new file mode 100644 index 0000000..1cbccdf --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/AppendMessageCallback.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 java.nio.ByteBuffer; +import org.apache.rocketmq.common.message.MessageExtBatch; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; + +/** + * Write messages callback interface + */ +public interface AppendMessageCallback { + + /** + * 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, PutMessageContext putMessageContext); + + /** + * 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, 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 new file mode 100644 index 0000000..98bf203 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/AppendMessageResult.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.store; + +import java.util.function.Supplier; + +/** + * When write a message to the commit log, returns results + */ +public class AppendMessageResult { + // Return code + private AppendMessageStatus status; + // Where to start writing + private long wroteOffset; + // Write Bytes + private int wroteBytes; + // Message ID + private String msgId; + private Supplier msgIdSupplier; + // Message storage timestamp + private long storeTimestamp; + // Consume queue's offset(step by one) + private long logicsOffset; + private long pagecacheRT = 0; + + private int msgNum = 1; + + public AppendMessageResult(AppendMessageStatus status) { + this(status, 0, 0, "", 0, 0, 0); + } + + public AppendMessageResult(AppendMessageStatus status, long wroteOffset, int wroteBytes, String msgId, + long storeTimestamp, long logicsOffset, long pagecacheRT) { + this.status = status; + this.wroteOffset = wroteOffset; + this.wroteBytes = wroteBytes; + this.msgId = msgId; + this.storeTimestamp = storeTimestamp; + this.logicsOffset = logicsOffset; + 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; + } + + public void setPagecacheRT(final long pagecacheRT) { + this.pagecacheRT = pagecacheRT; + } + + public boolean isOk() { + return this.status == AppendMessageStatus.PUT_OK; + } + + public AppendMessageStatus getStatus() { + return status; + } + + public void setStatus(AppendMessageStatus status) { + this.status = status; + } + + public long getWroteOffset() { + return wroteOffset; + } + + public void setWroteOffset(long wroteOffset) { + this.wroteOffset = wroteOffset; + } + + public int getWroteBytes() { + return wroteBytes; + } + + public void setWroteBytes(int wroteBytes) { + this.wroteBytes = wroteBytes; + } + + public String getMsgId() { + if (msgId == null && msgIdSupplier != null) { + msgId = msgIdSupplier.get(); + } + return msgId; + } + + public void setMsgId(String msgId) { + this.msgId = msgId; + } + + public long getStoreTimestamp() { + return storeTimestamp; + } + + public void setStoreTimestamp(long storeTimestamp) { + this.storeTimestamp = storeTimestamp; + } + + public long getLogicsOffset() { + return logicsOffset; + } + + public void setLogicsOffset(long logicsOffset) { + this.logicsOffset = logicsOffset; + } + + public int getMsgNum() { + return msgNum; + } + + public void setMsgNum(int msgNum) { + this.msgNum = msgNum; + } + + @Override + public String toString() { + return "AppendMessageResult{" + + "status=" + status + + ", wroteOffset=" + wroteOffset + + ", wroteBytes=" + wroteBytes + + ", msgId='" + msgId + '\'' + + ", storeTimestamp=" + storeTimestamp + + ", logicsOffset=" + logicsOffset + + ", pagecacheRT=" + pagecacheRT + + ", msgNum=" + msgNum + + '}'; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/AppendMessageStatus.java b/store/src/main/java/org/apache/rocketmq/store/AppendMessageStatus.java new file mode 100644 index 0000000..c3534d0 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/AppendMessageStatus.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.store; + +/** + * When write a message to the commit log, returns code + */ +public enum AppendMessageStatus { + PUT_OK, + END_OF_FILE, + MESSAGE_SIZE_EXCEEDED, + PROPERTIES_SIZE_EXCEEDED, + UNKNOWN_ERROR, + ROCKSDB_ERROR, +} diff --git a/store/src/main/java/org/apache/rocketmq/store/CommitLog.java b/store/src/main/java/org/apache/rocketmq/store/CommitLog.java new file mode 100644 index 0000000..75000b2 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/CommitLog.java @@ -0,0 +1,2496 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.google.common.base.Strings; +import java.net.Inet6Address; +import java.net.InetSocketAddress; +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.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.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.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.exception.ConsumeQueueException; +import org.apache.rocketmq.store.exception.StoreException; +import org.apache.rocketmq.store.ha.HAService; +import org.apache.rocketmq.store.ha.autoswitch.AutoSwitchHAService; +import org.apache.rocketmq.store.lock.AdaptiveBackOffSpinLockImpl; +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.ReferredIterator; +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 implements Swappable { + // Message's MAGIC CODE daa320a7 + 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 + 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; + + private final FlushManager flushManager; + private final ColdDataCheckService coldDataCheckService; + + private final AppendMessageCallback appendMessageCallback; + private final ThreadLocal putMessageThreadLocal; + + protected volatile long confirmOffset = -1L; + + private volatile long beginTimeInLock = 0; + + protected final PutMessageLock putMessageLock; + + protected final TopicQueueLock topicQueueLock; + + private volatile Set fullStorePaths = Collections.emptySet(); + + private final FlushDiskWatcher flushDiskWatcher; + + protected int commitLogSize; + + private final boolean enabledAppendPropCRC; + + 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.mappedFileQueue = new MappedFileQueue(storePath, + messageStore.getMessageStoreConfig().getMappedFileSizeCommitLog(), + messageStore.getAllocateMappedFileService()); + } + + this.defaultMessageStore = messageStore; + + this.flushManager = new DefaultFlushManager(); + this.coldDataCheckService = new ColdDataCheckService(); + + this.appendMessageCallback = new DefaultAppendMessageCallback(defaultMessageStore.getMessageStoreConfig()); + putMessageThreadLocal = new ThreadLocal() { + @Override + protected PutMessageThreadLocal initialValue() { + return new PutMessageThreadLocal(defaultMessageStore.getMessageStoreConfig()); + } + }; + + PutMessageLock adaptiveBackOffSpinLock = new AdaptiveBackOffSpinLockImpl(); + + this.putMessageLock = messageStore.getMessageStoreConfig().getUseABSLock() ? adaptiveBackOffSpinLock : + 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(); + } + + 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.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() { + this.flushManager.shutdown(); + log.info("shutdown commitLog successfully. storeRoot: {}", this.defaultMessageStore.getMessageStoreConfig().getStorePathRootDir()); + flushDiskWatcher.shutdown(true); + if (this.coldDataCheckService != null) { + this.coldDataCheckService.shutdown(); + } + } + + public long flush() { + this.mappedFileQueue.commit(0); + this.mappedFileQueue.flush(0); + return this.mappedFileQueue.getFlushedWhere(); + } + + public long getFlushedWhere() { + return this.mappedFileQueue.getFlushedWhere(); + } + + public long getMaxOffset() { + return this.mappedFileQueue.getMaxOffset(); + } + + public long remainHowManyDataToCommit() { + return this.mappedFileQueue.remainHowManyDataToCommit(); + } + + public long remainHowManyDataToFlush() { + return this.mappedFileQueue.remainHowManyDataToFlush(); + } + + public int deleteExpiredFile( + final long expiredTime, + final int deleteFilesInterval, + final long intervalForcibly, + final boolean 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); + } + + /** + * Read CommitLog data, use data replication + */ + public SelectMappedBufferResult getData(final long offset) { + return this.getData(offset, offset == 0); + } + + public SelectMappedBufferResult getData(final long offset, final boolean returnFirstOnNotFound) { + int mappedFileSize = this.defaultMessageStore.getMessageStoreConfig().getMappedFileSizeCommitLog(); + MappedFile mappedFile = this.mappedFileQueue.findMappedFileByOffset(offset, returnFirstOnNotFound); + if (mappedFile != null) { + int pos = (int) (offset % mappedFileSize); + SelectMappedBufferResult result = mappedFile.selectMappedBuffer(pos); + return result; + } + + 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(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()) { + int index = mappedFiles.size() - 1; + while (index > 0) { + MappedFile mappedFile = mappedFiles.get(index); + if (mappedFile.getFileFromOffset() <= maxPhyOffsetOfConsumeQueue) { + // It's safe to recover from this mapped file + break; + } + index--; + } + // TODO: Discuss if we need to load more commit-log mapped files into memory. + + MappedFile mappedFile = mappedFiles.get(index); + ByteBuffer byteBuffer = mappedFile.sliceByteBuffer(); + long processOffset = mappedFile.getFileFromOffset(); + long mappedFileOffset = 0; + long lastValidMsgPhyOffset = this.getConfirmOffset(); + while (true) { + DispatchRequest dispatchRequest = this.checkMessageAndReturnSize(byteBuffer, checkCRCOnRecover, checkDupInfo); + int size = dispatchRequest.getMsgSize(); + boolean doDispatch = dispatchRequest.getCommitLogOffset() > maxPhyOffsetOfConsumeQueue; + // 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 + log.info("recover last 3 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()); + } + } + // 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, + final boolean checkDupInfo) { + return this.checkMessageAndReturnSize(byteBuffer, checkCRC, checkDupInfo, true); + } + + private void doNothingForDeadCode(final Object obj) { + if (obj != null) { + log.debug(String.valueOf(obj.hashCode())); + } + } + + /** + * check the message and returns the message size + * + * @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 checkDupInfo, final boolean readBody) { + try { + if (byteBuffer.remaining() <= 4) { + return new DispatchRequest(-1, false /* fail */); + } + // 1 TOTAL SIZE + int totalSize = byteBuffer.getInt(); + if (byteBuffer.remaining() < totalSize - 4) { + return new DispatchRequest(-1, false /* fail */); + } + + // 2 MAGIC CODE + int magicCode = byteBuffer.getInt(); + switch (magicCode) { + case MessageDecoder.MESSAGE_MAGIC_CODE: + case MessageDecoder.MESSAGE_MAGIC_CODE_V2: + break; + case BLANK_MAGIC_CODE: + return new DispatchRequest(0, true /* success */); + default: + log.warn("found a illegal magic code 0x" + Integer.toHexString(magicCode)); + return new DispatchRequest(-1, false /* success */); + } + + MessageVersion messageVersion = MessageVersion.valueOfMagicCode(magicCode); + + byte[] bytesContent = new byte[totalSize]; + + int bodyCRC = byteBuffer.getInt(); + + int queueId = byteBuffer.getInt(); + + int flag = byteBuffer.getInt(); + + long queueOffset = byteBuffer.getLong(); + + long physicOffset = byteBuffer.getLong(); + + int sysFlag = byteBuffer.getInt(); + + long bornTimeStamp = byteBuffer.getLong(); + + 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; + 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(); + + long preparedTransactionOffset = byteBuffer.getLong(); + + int bodyLen = byteBuffer.getInt(); + if (bodyLen > 0) { + if (readBody) { + byteBuffer.get(bytesContent, 0, bodyLen); + + if (checkCRC) { + /** + * 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 { + byteBuffer.position(byteBuffer.position() + bodyLen); + } + } + + int topicLen = messageVersion.getTopicLength(byteBuffer); + byteBuffer.get(bytesContent, 0, topicLen); + String topic = new String(bytesContent, 0, topicLen, MessageDecoder.CHARSET_UTF8); + + long tagsCode = 0; + String keys = ""; + String uniqKey = null; + + short propertiesLength = byteBuffer.getShort(); + Map propertiesMap = null; + if (propertiesLength > 0) { + byteBuffer.get(bytesContent, 0, propertiesLength); + String properties = new String(bytesContent, 0, propertiesLength, MessageDecoder.CHARSET_UTF8); + propertiesMap = MessageDecoder.string2messageProperties(properties); + + keys = propertiesMap.get(MessageConst.PROPERTY_KEYS); + + 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 (!Strings.isNullOrEmpty(tags)) { + tagsCode = MessageExtBrokerInner.tagsString2tagsCode(MessageExt.parseTopicFilterType(sysFlag), tags); + } + + // Timing message processing + { + String t = propertiesMap.get(MessageConst.PROPERTY_DELAY_TIME_LEVEL); + if (TopicValidator.RMQ_SYS_SCHEDULE_TOPIC.equals(topic) && t != null) { + int delayLevel = Integer.parseInt(t); + + if (delayLevel > this.defaultMessageStore.getMaxDelayLevel()) { + delayLevel = this.defaultMessageStore.getMaxDelayLevel(); + } + + if (delayLevel > 0) { + tagsCode = this.defaultMessageStore.computeDeliverTimestamp(delayLevel, + storeTimestamp); + } + } + } + } + + 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); + doNothingForDeadCode(bornTimeStamp); + doNothingForDeadCode(byteBuffer1); + doNothingForDeadCode(byteBuffer2); + log.error( + "[BUG]read total count not equals msg total size. totalSize={}, readTotalCount={}, bodyLen={}, topicLen={}, propertiesLength={}", + totalSize, readLength, bodyLen, topicLen, propertiesLength); + return new DispatchRequest(totalSize, false/* success */); + } + + DispatchRequest dispatchRequest = new DispatchRequest( + topic, + queueId, + physicOffset, + totalSize, + tagsCode, + storeTimestamp, + queueOffset, + keys, + uniqKey, + sysFlag, + preparedTransactionOffset, + propertiesMap + ); + + setBatchSizeIfNeeded(propertiesMap, dispatchRequest); + + return dispatchRequest; + } catch (Exception e) { + log.error("checkMessageAndReturnSize failed, may can not dispatch", e); + } + + return new DispatchRequest(-1, false /* success */); + } + + 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() { + 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 this.defaultMessageStore.isSyncDiskFlush() ? getFlushedWhere() : 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; + } + + /** + * @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 + int index = mappedFiles.size() - 1; + MappedFile mappedFile = null; + for (; index >= 0; index--) { + mappedFile = mappedFiles.get(index); + if (this.isMappedFileMatchedRecover(mappedFile)) { + log.info("recover from this mapped file " + mappedFile.getFileName()); + break; + } + } + + if (index < 0) { + index = 0; + mappedFile = mappedFiles.get(index); + } + + 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, checkDupInfo); + int size = dispatchRequest.getMsgSize(); + + if (dispatchRequest.isSuccess()) { + // Normal data + if (size > 0) { + lastValidMsgPhyOffset = processOffset + mappedFileOffset; + mappedFileOffset += size; + + 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); + } + } + // 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; + } + } + + try { + this.getMessageStore().getQueueStore().flush(); + } catch (StoreException e) { + log.error("Failed to flush ConsumeQueueStore", e); + } + + 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); + } + // 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.getQueueStore().destroy(); + this.defaultMessageStore.getQueueStore().loadAfterDestroy(); + } + } + + public void truncateDirtyFiles(long phyOffset) { + if (phyOffset <= this.getFlushedWhere()) { + this.mappedFileQueue.setFlushedWhere(phyOffset); + } + + 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(); + + 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().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 { + 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; + } + + public boolean resetOffset(long offset) { + return this.mappedFileQueue.resetOffset(offset); + } + + public long getBeginTimeInLock() { + return beginTimeInLock; + } + + 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 + 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(); + 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); + } + + InetSocketAddress bornSocketAddress = (InetSocketAddress) msg.getBornHost(); + if (bornSocketAddress.getAddress() instanceof Inet6Address) { + msg.setBornHostV6Flag(); + } + + InetSocketAddress storeSocketAddress = (InetSocketAddress) msg.getStoreHost(); + if (storeSocketAddress.getAddress() instanceof Inet6Address) { + msg.setStoreHostAddressV6Flag(); + } + + PutMessageThreadLocal putMessageThreadLocal = this.putMessageThreadLocal.get(); + updateMaxMessageSize(putMessageThreadLocal); + String topicQueueKey = generateKey(putMessageThreadLocal.getKeyBuilder(), msg); + long elapsedTimeInLock = 0; + MappedFile unlockMappedFile = null; + MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(); + + long currOffset; + if (mappedFile == null) { + currOffset = 0; + } else { + currOffset = mappedFile.getFileFromOffset() + mappedFile.getWrotePosition(); + } + + int needAckNums = this.defaultMessageStore.getMessageStoreConfig().getInSyncReplicas(); + boolean needHandleHA = needHandleHA(msg); + + 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)); + } + } + + 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); + } + } + if (null == mappedFile) { + log.error("create mapped file1 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString()); + beginTimeInLock = 0; + 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)); + } + + 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 { + topicQueueLock.unlock(topicQueueKey); + } + + 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()) { + this.defaultMessageStore.unlockMappedFile(unlockMappedFile); + } + + PutMessageResult putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, result); + + // Statistics + storeStatsService.getSinglePutMessageTopicTimesTotal(msg.getTopic()).add(result.getMsgNum()); + storeStatsService.getSinglePutMessageTopicSizeTotal(topic).add(result.getWroteBytes()); + + return handleDiskFlushAndHA(putMessageResult, msg, needAckNums, needHandleHA); + } + + public CompletableFuture asyncPutMessages(final MessageExtBatch messageExtBatch) { + messageExtBatch.setStoreTimestamp(System.currentTimeMillis()); + AppendMessageResult result = null; + + StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService(); + + 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)); + } + + 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 elapsedTimeInLock = 0; + MappedFile unlockMappedFile = null; + MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(); + + long currOffset; + if (mappedFile == null) { + currOffset = 0; + } else { + currOffset = mappedFile.getFileFromOffset() + mappedFile.getWrotePosition(); + } + + int needAckNums = this.defaultMessageStore.getMessageStoreConfig().getInSyncReplicas(); + boolean needHandleHA = needHandleHA(messageExtBatch); + + 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 { + defaultMessageStore.assignOffset(messageExtBatch); + + putMessageLock.lock(); + try { + long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now(); + this.beginTimeInLock = beginLockTimestamp; + + // Here settings are stored timestamp, in order to ensure an orderly + // global + messageExtBatch.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); + } + } + if (null == mappedFile) { + log.error("Create mapped file1 error, topic: {} clientAddr: {}", messageExtBatch.getTopic(), messageExtBatch.getBornHostString()); + beginTimeInLock = 0; + 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(); + } + + // 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 { + topicQueueLock.unlock(topicQueueKey); + } + + 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()) { + this.defaultMessageStore.unlockMappedFile(unlockMappedFile); + } + + PutMessageResult putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, result); + + // Statistics + storeStatsService.getSinglePutMessageTopicTimesTotal(messageExtBatch.getTopic()).add(result.getMsgNum()); + storeStatsService.getSinglePutMessageTopicSizeTotal(messageExtBatch.getTopic()).add(result.getWroteBytes()); + + return handleDiskFlushAndHA(putMessageResult, messageExtBatch, needAckNums, needHandleHA); + } + + 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; + } + + 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() && offset + size <= this.getMaxOffset()) { + SelectMappedBufferResult result = this.getMessage(offset, size); + if (null != result) { + try { + 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(); + } + } + } + + return -1; + } + + public long getMinOffset() { + MappedFile mappedFile = this.mappedFileQueue.getFirstMappedFile(); + if (mappedFile != null) { + if (mappedFile.isAvailable()) { + return mappedFile.getFileFromOffset(); + } else { + return this.rollNextFile(mappedFile.getFileFromOffset()); + } + } + + return -1; + } + + public SelectMappedBufferResult getMessage(final long offset, final int size) { + int mappedFileSize = this.defaultMessageStore.getMessageStoreConfig().getMappedFileSizeCommitLog(); + MappedFile mappedFile = this.mappedFileQueue.findMappedFileByOffset(offset, offset == 0); + if (mappedFile != null) { + int pos = (int) (offset % mappedFileSize); + 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().getMappedFileSizeCommitLog(); + return offset + mappedFileSize - offset % mappedFileSize; + } + + public void destroy() { + this.mappedFileQueue.destroy(); + } + + public boolean appendData(long startOffset, byte[] data, int dataStart, int dataLength) { + putMessageLock.lock(); + try { + MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(startOffset); + if (null == mappedFile) { + log.error("appendData getLastMappedFile error " + startOffset); + return false; + } + + return mappedFile.appendMessage(data, dataStart, dataLength); + } finally { + putMessageLock.unlock(); + } + } + + public boolean retryDeleteFirstFile(final long intervalForcibly) { + return this.mappedFileQueue.retryDeleteFirstFile(intervalForcibly); + } + + public void checkSelf() { + mappedFileQueue.checkSelf(); + } + + public long lockTimeMills() { + long diff = 0; + long begin = this.beginTimeInLock; + if (begin > 0) { + diff = this.defaultMessageStore.now() - begin; + } + + if (diff < 0) { + diff = 0; + } + + 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; + } + + class CommitRealTimeService extends FlushCommitLogService { + + private long lastCommitTimestamp = 0; + + @Override + public String getServiceName() { + if (CommitLog.this.defaultMessageStore.getBrokerConfig().isInBrokerContainer()) { + return CommitLog.this.defaultMessageStore.getBrokerIdentity().getIdentifier() + CommitRealTimeService.class.getSimpleName(); + } + return CommitRealTimeService.class.getSimpleName(); + } + + @Override + public void run() { + CommitLog.log.info(this.getServiceName() + " service started"); + while (!this.isStopped()) { + int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitIntervalCommitLog(); + + int commitDataLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitCommitLogLeastPages(); + + int commitDataThoroughInterval = + CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitCommitLogThoroughInterval(); + + long begin = System.currentTimeMillis(); + if (begin >= (this.lastCommitTimestamp + commitDataThoroughInterval)) { + this.lastCommitTimestamp = begin; + commitDataLeastPages = 0; + } + + try { + boolean result = CommitLog.this.mappedFileQueue.commit(commitDataLeastPages); + long end = System.currentTimeMillis(); + if (!result) { + this.lastCommitTimestamp = end; // result = false means some data committed. + 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); + } + this.waitForRunning(interval); + } catch (Throwable e) { + CommitLog.log.error(this.getServiceName() + " service has exception. ", e); + } + } + + boolean result = false; + for (int i = 0; i < RETRY_TIMES_OVER && !result; i++) { + result = CommitLog.this.mappedFileQueue.commit(0); + CommitLog.log.info(this.getServiceName() + " service shutdown, retry " + (i + 1) + " times " + (result ? "OK" : "Not OK")); + } + CommitLog.log.info(this.getServiceName() + " service end"); + } + } + + class FlushRealTimeService extends FlushCommitLogService { + private long lastFlushTimestamp = 0; + private long printTimes = 0; + + @Override + public void run() { + CommitLog.log.info(this.getServiceName() + " service started"); + + while (!this.isStopped()) { + boolean flushCommitLogTimed = CommitLog.this.defaultMessageStore.getMessageStoreConfig().isFlushCommitLogTimed(); + + int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushIntervalCommitLog(); + int flushPhysicQueueLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogLeastPages(); + + int flushPhysicQueueThoroughInterval = + CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogThoroughInterval(); + + boolean printFlushProgress = false; + + // Print flush progress + long currentTimeMillis = System.currentTimeMillis(); + if (currentTimeMillis >= (this.lastFlushTimestamp + flushPhysicQueueThoroughInterval)) { + this.lastFlushTimestamp = currentTimeMillis; + flushPhysicQueueLeastPages = 0; + printFlushProgress = (printTimes++ % 10) == 0; + } + + try { + if (flushCommitLogTimed) { + Thread.sleep(interval); + } else { + this.waitForRunning(interval); + } + + if (printFlushProgress) { + this.printFlushProgress(); + } + + long begin = System.currentTimeMillis(); + CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages); + long storeTimestamp = CommitLog.this.mappedFileQueue.getStoreTimestamp(); + if (storeTimestamp > 0) { + 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); + } + } catch (Throwable e) { + CommitLog.log.warn(this.getServiceName() + " service has exception. ", e); + this.printFlushProgress(); + } + } + + // Normal shutdown, to ensure that all the flush before exit + boolean result = false; + for (int i = 0; i < RETRY_TIMES_OVER && !result; i++) { + result = CommitLog.this.mappedFileQueue.flush(0); + CommitLog.log.info(this.getServiceName() + " service shutdown, retry " + (i + 1) + " times " + (result ? "OK" : "Not OK")); + } + + this.printFlushProgress(); + + CommitLog.log.info(this.getServiceName() + " service end"); + } + + @Override + public String getServiceName() { + if (CommitLog.this.defaultMessageStore.getBrokerConfig().isInBrokerContainer()) { + return CommitLog.this.defaultMessageStore.getBrokerConfig().getIdentifier() + FlushRealTimeService.class.getSimpleName(); + } + return FlushRealTimeService.class.getSimpleName(); + } + + private void printFlushProgress() { + // CommitLog.log.info("how much disk fall behind memory, " + // + CommitLog.this.mappedFileQueue.howMuchFallBehind()); + } + + @Override + public long getJoinTime() { + return 1000 * 60 * 5; + } + } + + public static class GroupCommitRequest { + private final long nextOffset; + // 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, 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 int getAckNums() { + return ackNums; + } + + public long getDeadLine() { + return deadLine; + } + + public void wakeupCustomer(final PutMessageStatus status) { + this.flushOKFuture.complete(status); + } + + public CompletableFuture future() { + return flushOKFuture; + } + } + + /** + * GroupCommit Service + */ + class GroupCommitService extends FlushCommitLogService { + private volatile LinkedList requestsWrite = new LinkedList<>(); + private volatile LinkedList requestsRead = new LinkedList<>(); + private final PutMessageSpinLock lock = new PutMessageSpinLock(); + + 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() { + List tmp = this.requestsWrite; + this.requestsWrite = this.requestsRead; + this.requestsRead = tmp; + } + + private void doCommit() { + synchronized (this.requestsRead) { + if (!this.requestsRead.isEmpty()) { + for (GroupCommitRequest req : this.requestsRead) { + // 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 < 1000; i++) { + flushOK = CommitLog.this.mappedFileQueue.getFlushedWhere() >= req.getNextOffset(); + if (flushOK) { + break; + } else { + try { + Thread.sleep(1); + } catch (Throwable 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.clear(); + } + } + } + + public void run() { + CommitLog.log.info(this.getServiceName() + " service started"); + + while (!this.isStopped()) { + try { + this.waitForRunning(1); + 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); + } + + synchronized (this) { + 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() + GroupCheckService.class.getSimpleName(); + } + return GroupCheckService.class.getSimpleName(); + } + + @Override + public long getJoinTime() { + return 1000 * 60 * 5; + } + } + + 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; + // Store the message content + private final ByteBuffer msgStoreItemMemory; + private final int crc32ReservedLength; + private final MessageStoreConfig messageStoreConfig; + + DefaultAppendMessageCallback(MessageStoreConfig messageStoreConfig) { + this.msgStoreItemMemory = ByteBuffer.allocate(END_FILE_MIN_BLANK_LENGTH); + this.messageStoreConfig = messageStoreConfig; + this.crc32ReservedLength = messageStoreConfig.isEnabledAppendPropCRC() ? CommitLog.CRC32_RESERVED_LEN : 0; + } + + public AppendMessageResult handlePropertiesForLmqMsg(ByteBuffer preEncodeBuffer, + final MessageExtBrokerInner msgInner) { + if (msgInner.isEncodeCompleted()) { + return null; + } + + try { + LmqDispatch.wrapLmqDispatch(defaultMessageStore, msgInner); + } catch (ConsumeQueueException e) { + if (e.getCause() instanceof RocksDBException) { + log.error("Failed to wrap multi-dispatch", e); + return new AppendMessageResult(AppendMessageStatus.ROCKSDB_ERROR); + } + log.error("Failed to wrap multi-dispatch", e); + return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR); + } + + 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); + + 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, PutMessageContext putMessageContext) { + // STORETIMESTAMP + STOREHOSTADDRESS + OFFSET
    + + ByteBuffer preEncodeBuffer = msgInner.getEncodedBuff(); + boolean isMultiDispatchMsg = messageStoreConfig.isEnableLmq() && msgInner.needDispatchLMQ(); + 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(); + + 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 + 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 consume queue + 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; + } + + // Determines whether there is sufficient free space + if ((msgLen + END_FILE_MIN_BLANK_LENGTH) > maxBlank) { + this.msgStoreItemMemory.clear(); + // 1 TOTALSIZE + this.msgStoreItemMemory.putInt(maxBlank); + // 2 MAGICCODE + this.msgStoreItemMemory.putInt(CommitLog.BLANK_MAGIC_CODE); + // 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, 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); + } + + int pos = 4 // 1 TOTALSIZE + + 4 // 2 MAGICCODE + + 4 // 3 BODYCRC + + 4 // 4 QUEUEID + + 4; // 5 FLAG + // 6 QUEUEOFFSET + preEncodeBuffer.putLong(pos, queueOffset); + pos += 8; + // 7 PHYSICALOFFSET + preEncodeBuffer.putLong(pos, fileFromOffset + byteBuffer.position()); + pos += 8; + int ipLen = (msgInner.getSysFlag() & MessageSysFlag.BORNHOST_V6_FLAG) == 0 ? 4 + 4 : 16 + 4; + // 8 SYSFLAG, 9 BORNTIMESTAMP, 10 BORNHOST + pos += 4 + 8 + ipLen; + // 11 STORETIMESTAMP 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); // UtilAll.crc32 function will change the position to limit of the buffer + 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(preEncodeBuffer); + CommitLog.this.getMessageStore().getPerfCounter().endTick("WRITE_MEMORY_TIME_MS"); + msgInner.setEncodedBuff(null); + + if (isMultiDispatchMsg) { + try { + LmqDispatch.updateLmqOffsets(defaultMessageStore, msgInner); + } catch (ConsumeQueueException e) { + // Increase in-memory max offset of the queue should not fail. + return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR); + } + } + + 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, PutMessageContext putMessageContext) { + byteBuffer.mark(); + //physical offset + long wroteOffset = fileFromOffset + byteBuffer.position(); + // Record ConsumeQueue information + Long queueOffset = messageExtBatch.getQueueOffset(); + long beginQueueOffset = queueOffset; + int totalMsgLen = 0; + int msgNum = 0; + + final long beginTimeMills = CommitLog.this.defaultMessageStore.now(); + ByteBuffer messagesByteBuff = messageExtBatch.getEncodedBuff(); + + 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(); + + totalMsgLen += msgLen; + // Determines whether there is sufficient free space + if ((totalMsgLen + END_FILE_MIN_BLANK_LENGTH) > maxBlank) { + this.msgStoreItemMemory.clear(); + // 1 TOTALSIZE + this.msgStoreItemMemory.putInt(maxBlank); + // 2 MAGICCODE + this.msgStoreItemMemory.putInt(CommitLog.BLANK_MAGIC_CODE); + // 3 The remaining space may be any value + //ignore previous read + messagesByteBuff.reset(); + // 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, msgIdSupplier, messageExtBatch.getStoreTimestamp(), + beginQueueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills); + } + //move to add queue offset and commitlog offset + 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); + } + + messagesByteBuff.position(0); + messagesByteBuff.limit(totalMsgLen); + byteBuffer.put(messagesByteBuff); + messageExtBatch.setEncodedBuff(null); + AppendMessageResult result = new AppendMessageResult(AppendMessageStatus.PUT_OK, wroteOffset, totalMsgLen, msgIdSupplier, + messageExtBatch.getStoreTimestamp(), beginQueueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills); + result.setMsgNum(msgNum); + + return result; + } + + } + + 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(); + } + } + + @Override + 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()) { + if (defaultMessageStore.getMessageStoreConfig().isWakeFlushWhenPutMessage()) { + flushCommitLogService.wakeup(); + } + } else { + if (defaultMessageStore.getMessageStoreConfig().isWakeCommitWhenPutMessage()) { + 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()) { + if (defaultMessageStore.getMessageStoreConfig().isWakeFlushWhenPutMessage()) { + flushCommitLogService.wakeup(); + } + } else { + if (defaultMessageStore.getMessageStoreConfig().isWakeCommitWhenPutMessage()) { + 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 int getCommitLogSize() { + return commitLogSize; + } + + 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; + } + + 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(); + } + + @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()); + } + + 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); + } + }); + } + + 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 { + ConsumeQueueInterface consumeQueue = defaultMessageStore.findConsumeQueue(topic, queueId); + if (null == consumeQueue) { + return false; + } + ReferredIterator bufferConsumeQueue = consumeQueue.iterateFrom(offset, 1); + if (null == bufferConsumeQueue || !bufferConsumeQueue.hasNext()) { + return false; + } + return defaultMessageStore.checkInColdAreaByCommitOffset(bufferConsumeQueue.next().getPos(), 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 new file mode 100644 index 0000000..f3a7b7c --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/CommitLogDispatcher.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; + +import org.rocksdb.RocksDBException; + +/** + * Dispatcher of commit log. + */ +public interface CommitLogDispatcher { + + /** + * 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 0000000..1667144 --- /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 new file mode 100644 index 0000000..b6b9cff --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ConsumeQueue.java @@ -0,0 +1,1207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.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 MessageStore messageStore; + + private final MappedFileQueue mappedFileQueue; + private final String topic; + private final int queueId; + private final ByteBuffer byteBufferIndex; + + 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; + + public ConsumeQueue( + final String topic, + final int queueId, + final String storePath, + final int mappedFileSize, + final MessageStore messageStore) { + this.storePath = storePath; + this.mappedFileSize = mappedFileSize; + this.messageStore = messageStore; + + this.topic = topic; + this.queueId = queueId; + + String queueDir = this.storePath + + File.separator + topic + + File.separator + queueId; + + this.mappedFileQueue = new MappedFileQueue(queueDir, mappedFileSize, null); + + this.byteBufferIndex = ByteBuffer.allocate(CQ_STORE_UNIT_SIZE); + + if (messageStore.getMessageStoreConfig().isEnableConsumeQueueExt()) { + this.consumeQueueExt = new ConsumeQueueExt( + topic, + queueId, + 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")); + if (isExtReadEnable()) { + result &= this.consumeQueueExt.load(); + } + return result; + } + + @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; + long maxExtAddr = 1; + while (true) { + for (int i = 0; i < mappedFileSizeLogics; i += CQ_STORE_UNIT_SIZE) { + long offset = byteBuffer.getLong(); + int size = byteBuffer.getInt(); + long tagsCode = byteBuffer.getLong(); + + if (offset >= 0 && size > 0) { + mappedFileOffset = i + CQ_STORE_UNIT_SIZE; + this.setMaxPhysicOffset(offset + size); + if (isExtAddr(tagsCode)) { + maxExtAddr = tagsCode; + } + } else { + log.info("recover current consume queue file over, " + mappedFile.getFileName() + " " + + offset + " " + size + " " + tagsCode); + break; + } + } + + if (mappedFileOffset == mappedFileSizeLogics) { + index++; + if (index >= mappedFiles.size()) { + + log.info("recover last 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 consume queue file, " + mappedFile.getFileName()); + } + } else { + log.info("recover current consume queue over " + mappedFile.getFileName() + " " + + (processOffset + mappedFileOffset)); + break; + } + } + + processOffset += mappedFileOffset; + this.mappedFileQueue.setFlushedWhere(processOffset); + this.mappedFileQueue.setCommittedWhere(processOffset); + this.mappedFileQueue.truncateDirtyFiles(processOffset); + + if (isExtReadEnable()) { + this.consumeQueueExt.recover(); + log.info("Truncate consume queue extend file by max {}", maxExtAddr); + this.consumeQueueExt.truncateByMaxAddress(maxExtAddr); + } + } + } + + @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.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 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(); + 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); + phyOffset = byteBuffer.getLong(); + size = byteBuffer.getInt(); + if (phyOffset < minPhysicOffset) { + low = midOffset + CQ_STORE_UNIT_SIZE; + leftOffset = midOffset; + continue; + } + + 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; + break; + } else if (storeTime > timestamp) { + high = midOffset - CQ_STORE_UNIT_SIZE; + rightOffset = midOffset; + } else { + low = midOffset + CQ_STORE_UNIT_SIZE; + leftOffset = midOffset; + } + } + + 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 { + // 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; + } + + case UPPER: { + offset = leftOffset; + break; + } + default: { + log.warn("Unknown boundary type"); + break; + } + } + } + return (mappedFile.getFileFromOffset() + offset) / CQ_STORE_UNIT_SIZE; + } finally { + sbr.release(); + } + } + } + return 0; + } + + @Override + public void truncateDirtyLogicFiles(long phyOffset) { + truncateDirtyLogicFiles(phyOffset, true); + } + + public void truncateDirtyLogicFiles(long phyOffset, boolean deleteFile) { + + int logicFileSize = this.mappedFileSize; + + this.setMaxPhysicOffset(phyOffset); + long maxExtAddr = 1; + boolean shouldDeleteFile = false; + while (true) { + 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) { + long offset = byteBuffer.getLong(); + int size = byteBuffer.getInt(); + long tagsCode = byteBuffer.getLong(); + + if (0 == i) { + if (offset >= phyOffset) { + shouldDeleteFile = true; + break; + } else { + int pos = i + CQ_STORE_UNIT_SIZE; + mappedFile.setWrotePosition(pos); + mappedFile.setCommittedPosition(pos); + mappedFile.setFlushedPosition(pos); + this.setMaxPhysicOffset(offset + size); + // This maybe not take effect, when not every consume queue has extend file. + if (isExtAddr(tagsCode)) { + maxExtAddr = tagsCode; + } + } + } else { + + if (offset >= 0 && size > 0) { + + if (offset >= phyOffset) { + return; + } + + int pos = i + CQ_STORE_UNIT_SIZE; + mappedFile.setWrotePosition(pos); + mappedFile.setCommittedPosition(pos); + mappedFile.setFlushedPosition(pos); + this.setMaxPhysicOffset(offset + size); + if (isExtAddr(tagsCode)) { + maxExtAddr = tagsCode; + } + + if (pos == logicFileSize) { + return; + } + } else { + return; + } + } + } + + if (shouldDeleteFile) { + if (deleteFile) { + this.mappedFileQueue.deleteLastMappedFile(); + } else { + this.mappedFileQueue.deleteExpiredFile(Collections.singletonList(this.mappedFileQueue.getLastMappedFile())); + } + } + + } else { + break; + } + } + + if (isExtReadEnable()) { + this.consumeQueueExt.truncateByMaxAddress(maxExtAddr); + } + } + + @Override + public long getLastOffset() { + long lastOffset = -1; + + int logicFileSize = this.mappedFileSize; + + MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(); + if (mappedFile != null) { + + int position = mappedFile.getWrotePosition() - CQ_STORE_UNIT_SIZE; + if (position < 0) + position = 0; + + ByteBuffer byteBuffer = mappedFile.sliceByteBuffer(); + byteBuffer.position(position); + for (int i = 0; i < logicFileSize; i += CQ_STORE_UNIT_SIZE) { + long offset = byteBuffer.getLong(); + int size = byteBuffer.getInt(); + byteBuffer.getLong(); + + if (offset >= 0 && size > 0) { + lastOffset = offset + size; + } else { + break; + } + } + } + + return lastOffset; + } + + @Override + public boolean flush(final int flushLeastPages) { + boolean result = this.mappedFileQueue.flush(flushLeastPages); + if (isExtReadEnable()) { + result = result & this.consumeQueueExt.flush(flushLeastPages); + } + + return result; + } + + @Override + public int deleteExpiredFile(long offset) { + int cnt = this.mappedFileQueue.deleteExpiredFileByOffset(offset, CQ_STORE_UNIT_SIZE); + this.correctMinOffset(offset); + return cnt; + } + + /** + * 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) { + // 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(); + } + } + + if (isExtReadEnable()) { + this.consumeQueueExt.truncateByMinAddress(minExtAddr); + } + } + + @Override + public long getMinOffsetInQueue() { + return this.minLogicOffset / CQ_STORE_UNIT_SIZE; + } + + @Override + public void putMessagePositionInfoWrapper(DispatchRequest request) { + final int maxRetries = 30; + boolean canWrite = this.messageStore.getRunningFlags().isCQWriteable(); + for (int i = 0; i < maxRetries && canWrite; i++) { + long tagsCode = request.getTagsCode(); + if (isExtWriteEnable()) { + ConsumeQueueExt.CqExtUnit cqExtUnit = new ConsumeQueueExt.CqExtUnit(); + cqExtUnit.setFilterBitMap(request.getBitMap()); + cqExtUnit.setMsgStoreTime(request.getStoreTimestamp()); + cqExtUnit.setTagsCode(request.getTagsCode()); + + long extAddr = this.consumeQueueExt.put(cqExtUnit); + if (isExtAddr(extAddr)) { + tagsCode = extAddr; + } else { + log.warn("Save consume queue extend fail, So just save tagsCode! {}, topic:{}, queueId:{}, offset:{}", cqExtUnit, + topic, queueId, request.getCommitLogOffset()); + } + } + boolean result = this.putMessagePositionInfo(request.getCommitLogOffset(), + request.getMsgSize(), tagsCode, request.getConsumeQueueOffset()); + if (result) { + 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 + log.warn("[BUG]put commit log position info to " + 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("[BUG]consume queue can not write, {} {}", this.topic, this.queueId); + 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.LMQ_DISPATCH_SEPARATOR); + String[] queueOffsets = multiQueueOffset.split(MixAll.LMQ_DISPATCH_SEPARATOR); + 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 + size <= this.getMaxPhysicOffset()) { + log.warn("Maybe try to build consume queue repeatedly maxPhysicOffset={} phyOffset={}", this.getMaxPhysicOffset(), offset); + return true; + } + + this.byteBufferIndex.flip(); + this.byteBufferIndex.limit(CQ_STORE_UNIT_SIZE); + this.byteBufferIndex.putLong(offset); + this.byteBufferIndex.putInt(size); + this.byteBufferIndex.putLong(tagsCode); + + final long expectLogicOffset = cqOffset * CQ_STORE_UNIT_SIZE; + + MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(expectLogicOffset); + if (mappedFile != null) { + + if (mappedFile.isFirstCreateInQueue() && cqOffset != 0 && mappedFile.getWrotePosition() == 0) { + this.minLogicOffset = expectLogicOffset; + this.mappedFileQueue.setFlushedWhere(expectLogicOffset); + this.mappedFileQueue.setCommittedWhere(expectLogicOffset); + this.fillPreBlank(mappedFile, expectLogicOffset); + log.info("fill pre blank space " + mappedFile.getFileName() + " " + expectLogicOffset + " " + + mappedFile.getWrotePosition()); + } + + if (cqOffset != 0) { + long currentLogicOffset = mappedFile.getWrotePosition() + mappedFile.getFileFromOffset(); + + if (expectLogicOffset < currentLogicOffset) { + log.warn("Build consume queue repeatedly, expectLogicOffset: {} currentLogicOffset: {} Topic: {} QID: {} Diff: {}", + expectLogicOffset, currentLogicOffset, this.topic, this.queueId, expectLogicOffset - currentLogicOffset); + return true; + } + + if (expectLogicOffset != currentLogicOffset) { + LOG_ERROR.warn( + "[BUG]logic queue order maybe wrong, expectLogicOffset: {} currentLogicOffset: {} Topic: {} QID: {} Diff: {}", + expectLogicOffset, + currentLogicOffset, + this.topic, + this.queueId, + expectLogicOffset - currentLogicOffset + ); + } + } + this.setMaxPhysicOffset(offset + size); + boolean appendResult; + if (messageStore.getMessageStoreConfig().isPutConsumeQueueDataByFileChannel()) { + appendResult = mappedFile.appendMessageUsingFileChannel(this.byteBufferIndex.array()); + } else { + appendResult = mappedFile.appendMessage(this.byteBufferIndex.array()); + } + return appendResult; + } + return false; + } + + private void fillPreBlank(final MappedFile mappedFile, final long untilWhere) { + ByteBuffer byteBuffer = ByteBuffer.allocate(CQ_STORE_UNIT_SIZE); + byteBuffer.putLong(0L); + byteBuffer.putInt(Integer.MAX_VALUE); + byteBuffer.putLong(0L); + + int until = (int) (untilWhere % this.mappedFileQueue.getMappedFileSize()); + for (int i = 0; i < until; i += CQ_STORE_UNIT_SIZE) { + if (messageStore.getMessageStoreConfig().isPutConsumeQueueDataByFileChannel()) { + mappedFile.appendMessageUsingFileChannel(byteBuffer.array()); + } else { + mappedFile.appendMessage(byteBuffer.array()); + } + + } + } + + public SelectMappedBufferResult getIndexBuffer(final long startIndex) { + int mappedFileSize = this.mappedFileSize; + long offset = startIndex * CQ_STORE_UNIT_SIZE; + if (offset >= this.getMinLogicOffset()) { + MappedFile mappedFile = this.mappedFileQueue.findMappedFileByOffset(offset); + if (mappedFile != null) { + 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); + } + return null; + } + + public boolean getExt(final long offset, ConsumeQueueExt.CqExtUnit cqExtUnit) { + if (isExtReadEnable()) { + return this.consumeQueueExt.get(offset, cqExtUnit); + } + return false; + } + + @Override + public long getMinLogicOffset() { + return minLogicOffset; + } + + public void setMinLogicOffset(long minLogicOffset) { + this.minLogicOffset = minLogicOffset; + } + + @Override + public long rollNextFile(final long nextBeginOffset) { + int mappedFileSize = this.mappedFileSize; + int totalUnitsInFile = mappedFileSize / CQ_STORE_UNIT_SIZE; + 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; + } + + public void setMaxPhysicOffset(long maxPhysicOffset) { + this.maxPhysicOffset = maxPhysicOffset; + } + + @Override + public void destroy() { + this.setMaxPhysicOffset(-1); + this.minLogicOffset = 0; + this.mappedFileQueue.destroy(); + if (isExtReadEnable()) { + this.consumeQueueExt.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()) { + this.consumeQueueExt.checkSelf(); + } + } + + protected boolean isExtReadEnable() { + return this.consumeQueueExt != null; + } + + protected boolean isExtWriteEnable() { + return this.consumeQueueExt != null + && this.messageStore.getMessageStoreConfig().isEnableConsumeQueueExt(); + } + + /** + * Check {@code tagsCode} is address of extend file or tags code. + */ + 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 new file mode 100644 index 0000000..3f26637 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ConsumeQueueExt.java @@ -0,0 +1,621 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.constant.LoggerName; +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, + * such as message store time, filter bit map and etc. + *

    + *

  • 1. This class is used only by {@link ConsumeQueue}
  • + *
  • 2. And is weakly reliable.
  • + *
  • 3. Be careful, address returned is always less than 0.
  • + *
  • 4. Pls keep this file small.
  • + */ +public class ConsumeQueueExt { + private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + + private final MappedFileQueue mappedFileQueue; + private final String topic; + private final int queueId; + + private final String storePath; + private final int mappedFileSize; + private ByteBuffer tempContainer; + + public static final int END_BLANK_DATA_LENGTH = 4; + + /** + * Addr can not exceed this value.For compatible. + */ + public static final long MAX_ADDR = Integer.MIN_VALUE - 1L; + public static final long MAX_REAL_OFFSET = MAX_ADDR - Long.MIN_VALUE; + + /** + * Constructor. + * + * @param topic topic + * @param queueId id of queue + * @param storePath root dir of files to store. + * @param mappedFileSize file size + * @param bitMapLength bit map length. + */ + public ConsumeQueueExt(final String topic, + final int queueId, + final String storePath, + final int mappedFileSize, + final int bitMapLength) { + + this.storePath = storePath; + this.mappedFileSize = mappedFileSize; + + this.topic = topic; + this.queueId = queueId; + + String queueDir = this.storePath + + File.separator + topic + + File.separator + queueId; + + this.mappedFileQueue = new MappedFileQueue(queueDir, mappedFileSize, null); + + if (bitMapLength > 0) { + this.tempContainer = ByteBuffer.allocate( + bitMapLength / Byte.SIZE + ); + } + } + + public long getTotalSize() { + return this.mappedFileQueue.getTotalFileSize(); + } + + /** + * Check whether {@code address} point to extend file. + *

    + * Just test {@code address} is less than 0. + *

    + */ + public static boolean isExtAddr(final long address) { + return address <= MAX_ADDR; + } + + /** + * Transform {@code address}(decorated by {@link #decorate}) to offset in mapped file. + *

    + * if {@code address} is less than 0, return {@code address} - {@link java.lang.Long#MIN_VALUE}; + * else, just return {@code address} + *

    + */ + public long unDecorate(final long address) { + if (isExtAddr(address)) { + return address - Long.MIN_VALUE; + } + return address; + } + + /** + * Decorate {@code offset} from mapped file, in order to distinguish with tagsCode(saved in cq originally). + *

    + * if {@code offset} is greater than or equal to 0, then return {@code offset} + {@link java.lang.Long#MIN_VALUE}; + * else, just return {@code offset} + *

    + * + * @return ext address(value is less than 0) + */ + public long decorate(final long offset) { + if (!isExtAddr(offset)) { + return offset + Long.MIN_VALUE; + } + return offset; + } + + /** + * Get data from buffer. + * + * @param address less than 0 + */ + public CqExtUnit get(final long address) { + CqExtUnit cqExtUnit = new CqExtUnit(); + if (get(address, cqExtUnit)) { + return cqExtUnit; + } + + return null; + } + + /** + * Get data from buffer, and set to {@code cqExtUnit} + * + * @param address less than 0 + */ + public boolean get(final long address, final CqExtUnit cqExtUnit) { + if (!isExtAddr(address)) { + return false; + } + + final int mappedFileSize = this.mappedFileSize; + final long realOffset = unDecorate(address); + + MappedFile mappedFile = this.mappedFileQueue.findMappedFileByOffset(realOffset, realOffset == 0); + if (mappedFile == null) { + return false; + } + + int pos = (int) (realOffset % mappedFileSize); + + SelectMappedBufferResult bufferResult = mappedFile.selectMappedBuffer(pos); + if (bufferResult == null) { + log.warn("[BUG] Consume queue extend unit({}) is not found!", realOffset); + return false; + } + boolean ret = false; + try { + ret = cqExtUnit.read(bufferResult.getByteBuffer()); + } finally { + bufferResult.release(); + } + + return ret; + } + + /** + * Save to mapped buffer of file and return address. + *

    + * Be careful, this method is not thread safe. + *

    + * + * @return success: < 0: fail: >=0 + */ + public long put(final CqExtUnit cqExtUnit) { + final int retryTimes = 3; + try { + int size = cqExtUnit.calcUnitSize(); + if (size > CqExtUnit.MAX_EXT_UNIT_SIZE) { + log.error("Size of cq ext unit is greater than {}, {}", CqExtUnit.MAX_EXT_UNIT_SIZE, cqExtUnit); + return 1; + } + if (this.mappedFileQueue.getMaxOffset() + size > MAX_REAL_OFFSET) { + log.warn("Capacity of ext is maximum!{}, {}", this.mappedFileQueue.getMaxOffset(), size); + return 1; + } + // unit size maybe change.but, the same most of the time. + if (this.tempContainer == null || this.tempContainer.capacity() < size) { + this.tempContainer = ByteBuffer.allocate(size); + } + + for (int i = 0; i < retryTimes; i++) { + MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(); + + if (mappedFile == null || mappedFile.isFull()) { + mappedFile = this.mappedFileQueue.getLastMappedFile(0); + } + + if (mappedFile == null) { + log.error("Create mapped file when save consume queue extend, {}", cqExtUnit); + continue; + } + final int wrotePosition = mappedFile.getWrotePosition(); + final int blankSize = this.mappedFileSize - wrotePosition - END_BLANK_DATA_LENGTH; + + // check whether has enough space. + if (size > blankSize) { + fullFillToEnd(mappedFile, wrotePosition); + log.info("No enough space(need:{}, has:{}) of file {}, so fill to end", + size, blankSize, mappedFile.getFileName()); + continue; + } + + if (mappedFile.appendMessage(cqExtUnit.write(this.tempContainer), 0, size)) { + return decorate(wrotePosition + mappedFile.getFileFromOffset()); + } + } + } catch (Throwable e) { + log.error("Save consume queue extend error, " + cqExtUnit, e); + } + + return 1; + } + + protected void fullFillToEnd(final MappedFile mappedFile, final int wrotePosition) { + ByteBuffer mappedFileBuffer = mappedFile.sliceByteBuffer(); + mappedFileBuffer.position(wrotePosition); + + // ending. + mappedFileBuffer.putShort((short) -1); + + mappedFile.setWrotePosition(this.mappedFileSize); + } + + /** + * Load data from file when startup. + */ + public boolean load() { + boolean result = this.mappedFileQueue.load(); + log.info("load consume queue extend" + this.topic + "-" + this.queueId + " " + (result ? "OK" : "Failed")); + return result; + } + + /** + * Check whether the step size in mapped file queue is correct. + */ + public void checkSelf() { + this.mappedFileQueue.checkSelf(); + } + + /** + * Recover. + */ + public void recover() { + final List mappedFiles = this.mappedFileQueue.getMappedFiles(); + if (mappedFiles == null || mappedFiles.isEmpty()) { + return; + } + + // load all files, consume queue will truncate extend files. + int index = 0; + + MappedFile mappedFile = mappedFiles.get(index); + ByteBuffer byteBuffer = mappedFile.sliceByteBuffer(); + long processOffset = mappedFile.getFileFromOffset(); + long mappedFileOffset = 0; + CqExtUnit extUnit = new CqExtUnit(); + while (true) { + extUnit.readBySkip(byteBuffer); + + // check whether write sth. + if (extUnit.getSize() > 0) { + mappedFileOffset += extUnit.getSize(); + continue; + } + + index++; + if (index < mappedFiles.size()) { + mappedFile = mappedFiles.get(index); + byteBuffer = mappedFile.sliceByteBuffer(); + processOffset = mappedFile.getFileFromOffset(); + mappedFileOffset = 0; + log.info("Recover next consume queue extend file, " + mappedFile.getFileName()); + continue; + } + + log.info("All files of consume queue extend has been recovered over, last mapped file " + + mappedFile.getFileName()); + break; + } + + processOffset += mappedFileOffset; + this.mappedFileQueue.setFlushedWhere(processOffset); + this.mappedFileQueue.setCommittedWhere(processOffset); + this.mappedFileQueue.truncateDirtyFiles(processOffset); + } + + /** + * Delete files before {@code minAddress}. + * + * @param minAddress less than 0 + */ + public void truncateByMinAddress(final long minAddress) { + if (!isExtAddr(minAddress)) { + return; + } + + log.info("Truncate consume queue ext by min {}.", minAddress); + + List willRemoveFiles = new ArrayList<>(); + + List mappedFiles = this.mappedFileQueue.getMappedFiles(); + final long realOffset = unDecorate(minAddress); + + for (MappedFile file : mappedFiles) { + long fileTailOffset = file.getFileFromOffset() + this.mappedFileSize; + + if (fileTailOffset < realOffset) { + log.info("Destroy consume queue ext by min: file={}, fileTailOffset={}, minOffset={}", file.getFileName(), + fileTailOffset, realOffset); + if (file.destroy(1000)) { + willRemoveFiles.add(file); + } + } + } + + this.mappedFileQueue.deleteExpiredFile(willRemoveFiles); + } + + /** + * Delete files after {@code maxAddress}, and reset wrote/commit/flush position to last file. + * + * @param maxAddress less than 0 + */ + public void truncateByMaxAddress(final long maxAddress) { + if (!isExtAddr(maxAddress)) { + return; + } + + log.info("Truncate consume queue ext by max {}.", maxAddress); + + CqExtUnit cqExtUnit = get(maxAddress); + if (cqExtUnit == null) { + log.error("[BUG] address {} of consume queue extend not found!", maxAddress); + return; + } + + final long realOffset = unDecorate(maxAddress); + + this.mappedFileQueue.truncateDirtyFiles(realOffset + cqExtUnit.getSize()); + } + + /** + * flush buffer to file. + */ + public boolean flush(final int flushLeastPages) { + return this.mappedFileQueue.flush(flushLeastPages); + } + + /** + * delete files and directory. + */ + public void destroy() { + this.mappedFileQueue.destroy(); + } + + /** + * Max address(value is less than 0). + *

    + *

    + * Be careful: it's an address just when invoking this method. + *

    + */ + public long getMaxAddress() { + MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(); + if (mappedFile == null) { + return decorate(0); + } + return decorate(mappedFile.getFileFromOffset() + mappedFile.getWrotePosition()); + } + + /** + * Minus address saved in file. + */ + public long getMinAddress() { + MappedFile firstFile = this.mappedFileQueue.getFirstMappedFile(); + if (firstFile == null) { + return decorate(0); + } + return decorate(firstFile.getFileFromOffset()); + } + + /** + * Store unit. + */ + public static class CqExtUnit { + public static final short MIN_EXT_UNIT_SIZE + = 2 * 1 // size, 32k max + + 8 * 2 // msg time + tagCode + + 2; // bitMapSize + + public static final int MAX_EXT_UNIT_SIZE = Short.MAX_VALUE; + + public CqExtUnit() { + } + + public CqExtUnit(Long tagsCode, long msgStoreTime, byte[] filterBitMap) { + this.tagsCode = tagsCode == null ? 0 : tagsCode; + this.msgStoreTime = msgStoreTime; + this.filterBitMap = filterBitMap; + this.bitMapSize = (short) (filterBitMap == null ? 0 : filterBitMap.length); + this.size = (short) (MIN_EXT_UNIT_SIZE + this.bitMapSize); + } + + /** + * unit size + */ + private short size; + /** + * has code of tags + */ + private long tagsCode; + /** + * the time to store into commit log of message + */ + private long msgStoreTime; + /** + * size of bit map + */ + private short bitMapSize; + /** + * filter bit map + */ + private byte[] filterBitMap; + + /** + * build unit from buffer from current position. + */ + private boolean read(final ByteBuffer buffer) { + if (buffer.position() + 2 > buffer.limit()) { + return false; + } + + this.size = buffer.getShort(); + + if (this.size < 1) { + return false; + } + + this.tagsCode = buffer.getLong(); + this.msgStoreTime = buffer.getLong(); + this.bitMapSize = buffer.getShort(); + + if (this.bitMapSize < 1) { + return true; + } + + if (this.filterBitMap == null || this.filterBitMap.length != this.bitMapSize) { + this.filterBitMap = new byte[bitMapSize]; + } + + buffer.get(this.filterBitMap); + return true; + } + + /** + * Only read first 2 byte to get unit size. + *

    + * if size > 0, then skip buffer position with size. + *

    + *

    + * if size <= 0, nothing to do. + *

    + */ + private void readBySkip(final ByteBuffer buffer) { + ByteBuffer temp = buffer.slice(); + + short tempSize = temp.getShort(); + this.size = tempSize; + + if (tempSize > 0) { + buffer.position(buffer.position() + this.size); + } + } + + /** + * Transform unit data to byte array. + *

    + *

  • 1. @{code container} can be null, it will be created if null.
  • + *
  • 2. if capacity of @{code container} is less than unit size, it will be created also.
  • + *
  • 3. Pls be sure that size of unit is not greater than {@link #MAX_EXT_UNIT_SIZE}
  • + */ + private byte[] write(final ByteBuffer container) { + this.bitMapSize = (short) (filterBitMap == null ? 0 : filterBitMap.length); + this.size = (short) (MIN_EXT_UNIT_SIZE + this.bitMapSize); + + ByteBuffer temp = container; + + if (temp == null || temp.capacity() < this.size) { + temp = ByteBuffer.allocate(this.size); + } + + temp.flip(); + temp.limit(this.size); + + temp.putShort(this.size); + temp.putLong(this.tagsCode); + temp.putLong(this.msgStoreTime); + temp.putShort(this.bitMapSize); + if (this.bitMapSize > 0) { + temp.put(this.filterBitMap); + } + + return temp.array(); + } + + /** + * Calculate unit size by current data. + */ + private int calcUnitSize() { + int sizeTemp = MIN_EXT_UNIT_SIZE + (filterBitMap == null ? 0 : filterBitMap.length); + return sizeTemp; + } + + public long getTagsCode() { + return tagsCode; + } + + public void setTagsCode(final long tagsCode) { + this.tagsCode = tagsCode; + } + + public long getMsgStoreTime() { + return msgStoreTime; + } + + public void setMsgStoreTime(final long msgStoreTime) { + this.msgStoreTime = msgStoreTime; + } + + public byte[] getFilterBitMap() { + if (this.bitMapSize < 1) { + return null; + } + return filterBitMap; + } + + public void setFilterBitMap(final byte[] filterBitMap) { + this.filterBitMap = filterBitMap; + // not safe transform, but size will be calculate by #calcUnitSize + this.bitMapSize = (short) (filterBitMap == null ? 0 : filterBitMap.length); + } + + public short getSize() { + return size; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof CqExtUnit)) + return false; + + CqExtUnit cqExtUnit = (CqExtUnit) o; + + if (bitMapSize != cqExtUnit.bitMapSize) + return false; + if (msgStoreTime != cqExtUnit.msgStoreTime) + return false; + if (size != cqExtUnit.size) + return false; + if (tagsCode != cqExtUnit.tagsCode) + return false; + if (!Arrays.equals(filterBitMap, cqExtUnit.filterBitMap)) + return false; + + return true; + } + + @Override + public int hashCode() { + int result = (int) size; + result = 31 * result + (int) (tagsCode ^ (tagsCode >>> 32)); + result = 31 * result + (int) (msgStoreTime ^ (msgStoreTime >>> 32)); + result = 31 * result + (int) bitMapSize; + result = 31 * result + (filterBitMap != null ? Arrays.hashCode(filterBitMap) : 0); + return result; + } + + @Override + public String toString() { + return "CqExtUnit{" + + "size=" + size + + ", tagsCode=" + tagsCode + + ", msgStoreTime=" + msgStoreTime + + ", bitMapSize=" + bitMapSize + + ", filterBitMap=" + Arrays.toString(filterBitMap) + + '}'; + } + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/DefaultMessageFilter.java b/store/src/main/java/org/apache/rocketmq/store/DefaultMessageFilter.java new file mode 100644 index 0000000..fff6966 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/DefaultMessageFilter.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; + +import java.nio.ByteBuffer; +import java.util.Map; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; + +public class DefaultMessageFilter implements MessageFilter { + + private SubscriptionData subscriptionData; + + public DefaultMessageFilter(final SubscriptionData subscriptionData) { + this.subscriptionData = subscriptionData; + } + + @Override + public boolean isMatchedByConsumeQueue(Long tagsCode, ConsumeQueueExt.CqExtUnit cqExtUnit) { + if (null == tagsCode || null == subscriptionData) { + return true; + } + + if (subscriptionData.isClassFilterMode()) { + return true; + } + + return subscriptionData.getSubString().equals(SubscriptionData.SUB_ALL) + || subscriptionData.getCodeSet().contains(tagsCode.intValue()); + } + + @Override + public boolean isMatchedByCommitLog(ByteBuffer msgBuffer, Map properties) { + return true; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java b/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java new file mode 100644 index 0000000..360523b --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java @@ -0,0 +1,3390 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +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.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.commons.validator.routines.InetAddressValidator; +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.exception.ConsumeQueueException; +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.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.apache.rocketmq.store.timer.TimerMessageStore; +import org.apache.rocketmq.store.util.PerfCounter; +import org.rocksdb.RocksDBException; + +public class DefaultMessageStore implements MessageStore { + 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 + protected final CommitLog commitLog; + + protected final ConsumeQueueStoreInterface consumeQueueStore; + + private final FlushConsumeQueueService flushConsumeQueueService; + + protected final CleanCommitLogService cleanCommitLogService; + + private final CleanConsumeQueueService cleanConsumeQueueService; + + private final CorrectLogicOffsetService correctLogicOffsetService; + + protected final IndexService indexService; + + private final AllocateMappedFileService allocateMappedFileService; + + private ReputMessageService reputMessageService; + + private HAService haService; + + // CompactionLog + private CompactionStore compactionStore; + + private CompactionService compactionService; + + private final StoreStatsService storeStatsService; + + private final TransientStorePool transientStorePool; + + protected final RunningFlags runningFlags = new RunningFlags(); + private final SystemClock systemClock = new SystemClock(); + + 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; + + protected StoreCheckpoint storeCheckpoint; + private TimerMessageStore timerMessageStore; + + private final LinkedList dispatcherList; + + private RocksDBMessageStore rocksDBMessageStore; + + private final RandomAccessFile lockFile; + + 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 final 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 final int dispatchRequestOrderlyQueueSize = 16; + + private final DispatchRequestOrderlyQueue dispatchRequestOrderlyQueue = new DispatchRequestOrderlyQueue(dispatchRequestOrderlyQueueSize); + + private long stateMachineVersion = 0L; + + // this is a unmodifiableMap + private final 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, + 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); + if (messageStoreConfig.isEnableDLegerCommitLog()) { + this.commitLog = new DLedgerCommitLog(this); + } else { + this.commitLog = new CommitLog(this); + } + + this.consumeQueueStore = createConsumeQueueStore(); + + this.flushConsumeQueueService = createFlushConsumeQueueService(); + this.cleanCommitLogService = new CleanCommitLogService(); + this.cleanConsumeQueueService = createCleanConsumeQueueService(); + this.correctLogicOffsetService = createCorrectLogicOffsetService(); + this.storeStatsService = new StoreStatsService(getBrokerIdentity()); + this.indexService = new IndexService(this); + + 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.isEnableBuildConsumeQueueConcurrently()) { + this.reputMessageService = new ReputMessageService(); + } else { + this.reputMessageService = new ConcurrentReputMessageService(); + } + + this.transientStorePool = new TransientStorePool(messageStoreConfig.getTransientStorePoolSize(), messageStoreConfig.getMappedFileSizeCommitLog()); + + 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())); + 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 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 = 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(); + LOGGER.info("last shutdown {}, store path root dir: {}", + lastExitOK ? "normally" : "abnormally", messageStoreConfig.getStorePathRootDir()); + + // load Commit Log + result = this.commitLog.load(); + + // load Consume Queue + result = result && this.consumeQueueStore.load(); + + if (messageStoreConfig.isEnableCompaction()) { + result = result && this.compactionService.load(lastExitOK); + } + + if (result) { + loadCheckPoint(); + result = this.indexService.load(lastExitOK); + this.recover(lastExitOK); + 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) { + LOGGER.error("load exception", e); + result = false; + } + + if (!result) { + this.allocateMappedFileService.shutdown(); + } + + return result; + } + + public void loadCheckPoint() throws IOException { + this.storeCheckpoint = + new StoreCheckpoint( + StorePathConfigHelper.getStoreCheckpoint(this.messageStoreConfig.getStorePathRootDir())); + this.masterFlushedOffset = this.storeCheckpoint.getMasterFlushedOffset(); + setConfirmOffset(this.storeCheckpoint.getConfirmPhyOffset()); + } + + /** + * @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(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.haService != null) { + 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) { + LOGGER.error("shutdown Exception, ", e); + } + + if (this.haService != null) { + this.haService.shutdown(); + } + + this.storeStatsService.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(); + } + if (this.rocksDBMessageStore != null && this.rocksDBMessageStore.consumeQueueStore != null) { + this.rocksDBMessageStore.consumeQueueStore.shutdown(); + } + this.flushConsumeQueueService.shutdown(); + this.allocateMappedFileService.shutdown(); + this.storeCheckpoint.shutdown(); + + this.perfs.shutdown(); + + if (this.runningFlags.isWriteable() && dispatchBehindBytes() == 0) { + this.deleteFile(StorePathConfigHelper.getAbortFile(this.messageStoreConfig.getStorePathRootDir())); + shutDownNormal = true; + } else { + LOGGER.warn("the store may be wrong, so shutdown abnormally, and keep abort file."); + } + } + + this.transientStorePool.destroy(); + + if (lockFile != null && lock != null) { + try { + lock.release(); + lockFile.close(); + } catch (IOException e) { + } + } + } + + @Override + public void destroy() { + this.consumeQueueStore.destroy(); + this.commitLog.destroy(); + this.indexService.destroy(); + this.deleteFile(StorePathConfigHelper.getAbortFile(this.messageStoreConfig.getStorePathRootDir())); + this.deleteFile(StorePathConfigHelper.getStoreCheckpoint(this.messageStoreConfig.getStorePathRootDir())); + } + + public long getMajorFileSize() { + long commitLogSize = 0; + if (this.commitLog != null) { + commitLogSize = this.commitLog.getTotalSize(); + } + + long consumeQueueSize = 0; + if (this.consumeQueueStore != null) { + consumeQueueSize = this.consumeQueueStore.getTotalSize(); + } + + long indexFileSize = 0; + if (this.indexService != null) { + indexFileSize = this.indexService.getTotalSize(); + } + + return commitLogSize + consumeQueueSize + indexFileSize; + } + + @Override + public CompletableFuture asyncPutMessage(MessageExtBrokerInner msg) { + + for (PutMessageHook putMessageHook : putMessageHookList) { + PutMessageResult handleResult = putMessageHook.executeBeforePutMessage(msg); + if (handleResult != null) { + return CompletableFuture.completedFuture(handleResult); + } + } + + 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 (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(); + CompletableFuture putResultFuture = this.commitLog.asyncPutMessage(msg); + + 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().add(1); + } + }); + + return putResultFuture; + } + + @Override + public CompletableFuture asyncPutMessages(MessageExtBatch messageExtBatch) { + + for (PutMessageHook putMessageHook : putMessageHookList) { + PutMessageResult handleResult = putMessageHook.executeBeforePutMessage(messageExtBatch); + if (handleResult != null) { + return CompletableFuture.completedFuture(handleResult); + } + } + + long beginTime = this.getSystemClock().now(); + CompletableFuture putResultFuture = this.commitLog.asyncPutMessages(messageExtBatch); + + 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 (null == result || !result.isOk()) { + this.storeStatsService.getPutMessageFailedTimes().add(1); + } + }); + + return putResultFuture; + } + + @Override + public PutMessageResult putMessage(MessageExtBrokerInner msg) { + return waitForPutResult(asyncPutMessage(msg)); + } + + @Override + public PutMessageResult putMessages(MessageExtBatch messageExtBatch) { + return waitForPutResult(asyncPutMessages(messageExtBatch)); + } + + 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); + } + } + + @Override + public boolean isOSPageCacheBusy() { + long begin = this.getCommitLog().getBeginTimeInLock(); + long diff = this.systemClock.now() - begin; + + return diff < 10000000 + && diff > this.messageStoreConfig.getOsPageCacheBusyTimeOutMills(); + } + + @Override + 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 int maxTotalMsgSize, final MessageFilter messageFilter) { + if (this.shutdown) { + LOGGER.warn("message store has shutdown, so getMessage is forbidden"); + return null; + } + + if (!this.runningFlags.isReadable()) { + 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; + long nextBeginOffset = offset; + long minOffset = 0; + long maxOffset = 0; + + GetMessageResult getResult = new GetMessageResult(); + int filterMessageCount = 0; + + final long maxOffsetPy = this.commitLog.getMaxOffset(); + + ConsumeQueueInterface consumeQueue = findConsumeQueue(topic, queueId); + 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 < minOffset) { + status = GetMessageStatus.OFFSET_TOO_SMALL; + nextBeginOffset = nextOffsetCorrection(offset, minOffset); + } else if (offset == maxOffset) { + status = GetMessageStatus.OFFSET_OVERFLOW_ONE; + nextBeginOffset = nextOffsetCorrection(offset, offset); + } else if (offset > maxOffset) { + status = GetMessageStatus.OFFSET_OVERFLOW_BADLY; + nextBeginOffset = nextOffsetCorrection(offset, maxOffset); + } else { + 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 { + 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; + while (bufferConsumeQueue.hasNext() + && nextBeginOffset < maxOffset) { + CqUnit cqUnit = bufferConsumeQueue.next(); + long offsetPy = cqUnit.getPos(); + int sizePy = cqUnit.getSize(); + + boolean isInMem = estimateInMemByCommitOffset(offsetPy, maxOffsetPy); + + if ((cqUnit.getQueueOffset() - offset) * consumeQueue.getUnitSize() >= maxFilterMessageSize) { + break; + } + + if (this.isTheBatchFull(sizePy, cqUnit.getBatchNum(), maxMsgNums, maxPullSize, getResult.getBufferTotalSize(), getResult.getMessageCount(), isInMem)) { + 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; + } + } + + if (messageFilter != null + && !messageFilter.isMatchedByConsumeQueue(cqUnit.getValidTagsCodeAsLong(), cqUnit.getCqExtUnit())) { + if (getResult.getBufferTotalSize() == 0) { + status = GetMessageStatus.NO_MATCHED_MESSAGE; + } + + continue; + } + + SelectMappedBufferResult selectResult = this.commitLog.getMessage(offsetPy, sizePy); + if (null == selectResult) { + if (getResult.getBufferTotalSize() == 0) { + status = GetMessageStatus.MESSAGE_WAS_REMOVING; + } + + nextPhyFileStartOffset = this.commitLog.rollNextFile(offsetPy); + continue; + } + + if (messageStoreConfig.isColdDataFlowControlEnable() && !MixAll.isSysConsumerGroupPullMessage(group) && !selectResult.isInCache()) { + getResult.setColdDataSum(getResult.getColdDataSum() + sizePy); + } + + if (messageFilter != null + && !messageFilter.isMatchedByCommitLog(selectResult.getByteBuffer().slice(), null)) { + if (getResult.getBufferTotalSize() == 0) { + status = GetMessageStatus.NO_MATCHED_MESSAGE; + } + // release... + selectResult.release(); + filterMessageCount++; + continue; + } + this.storeStatsService.getGetMessageTransferredMsgCount().add(cqUnit.getBatchNum()); + getResult.addMessage(selectResult, cqUnit.getQueueOffset(), cqUnit.getBatchNum()); + status = GetMessageStatus.FOUND; + nextPhyFileStartOffset = Long.MIN_VALUE; + } + } catch (RocksDBException e) { + ERROR_LOG.error("getMessage Failed. cid: {}, topic: {}, queueId: {}, offset: {}, minOffset: {}, maxOffset: {}, {}", + group, topic, queueId, offset, minOffset, maxOffset, e.getMessage()); + } finally { + if (bufferConsumeQueue != null) { + bufferConsumeQueue.release(); + } + } + } + + 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; + nextBeginOffset = nextOffsetCorrection(offset, 0); + } + + if (GetMessageStatus.FOUND == status) { + this.storeStatsService.getGetMessageTimesTotalFound().add(1); + } else { + this.storeStatsService.getGetMessageTimesTotalMiss().add(1); + } + + if (this.messageStoreConfig.isDiskFallRecorded() && GetMessageStatus.OFFSET_OVERFLOW_ONE == status) { + brokerStatsManager.recordDiskFallBehindSize(group, topic, queueId, 0); + brokerStatsManager.recordDiskFallBehindTime(group, topic, queueId, 0); + } + + long elapsedTime = this.getSystemClock().now() - beginTime; + this.storeStatsService.setGetMessageEntireTimeMax(elapsedTime); + + // lazy init no data found. + if (getResult == null) { + getResult = new GetMessageResult(0); + } + + getResult.setStatus(status); + getResult.setNextBeginOffset(nextBeginOffset); + getResult.setMaxOffset(maxOffset); + getResult.setMinOffset(minOffset); + getResult.setFilterMessageCount(filterMessageCount); + 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) throws ConsumeQueueException { + return getMaxOffsetInQueue(topic, queueId, true); + } + + @Override + public long getMaxOffsetInQueue(String topic, int queueId, boolean committed) throws ConsumeQueueException { + if (committed) { + ConsumeQueueInterface logic = this.getConsumeQueue(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) { + try { + return this.consumeQueueStore.getMinOffsetInQueue(topic, queueId); + } catch (RocksDBException e) { + ERROR_LOG.error("getMinOffsetInQueue Failed. topic: {}, queueId: {}", topic, queueId, e); + 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) { + ConsumeQueueInterface consumeQueue = getConsumeQueue(topic, queueId); + if (consumeQueue != null) { + CqUnit cqUnit = consumeQueue.get(consumeQueueOffset); + if (cqUnit != null) { + return cqUnit.getPos(); + } + } + return 0; + } + + @Override + public long getOffsetInQueueByTime(String topic, int queueId, long 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) { + try { + // 1 TOTALSIZE + int size = sbr.getByteBuffer().getInt(); + return lookMessageByOffset(commitLogOffset, size); + } finally { + sbr.release(); + } + } + + return null; + } + + @Override + public SelectMappedBufferResult selectOneMessageByOffset(long commitLogOffset) { + SelectMappedBufferResult sbr = this.commitLog.getMessage(commitLogOffset, 4); + if (null != sbr) { + try { + // 1 TOTALSIZE + int size = sbr.getByteBuffer().getInt(); + return this.commitLog.getMessage(commitLogOffset, size); + } finally { + sbr.release(); + } + } + + return null; + } + + @Override + public SelectMappedBufferResult selectOneMessageByOffset(long commitLogOffset, int msgSize) { + 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(); + + { + 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)); + } + + { + double logicsRatio = UtilAll.getDiskPartitionSpaceUsedPercent(getStorePathLogic()); + result.put(RunningStats.consumeQueueDiskRatio.name(), String.valueOf(logicsRatio)); + } + + result.put(RunningStats.commitLogMinOffset.name(), String.valueOf(DefaultMessageStore.this.getMinPhyOffset())); + result.put(RunningStats.commitLogMaxOffset.name(), String.valueOf(DefaultMessageStore.this.getMaxPhyOffset())); + + return result; + } + + @Override + public long getMaxPhyOffset() { + return this.commitLog.getMaxOffset(); + } + + @Override + 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) { + ConsumeQueueInterface logicQueue = this.getConsumeQueue(topic, queueId); + if (logicQueue != null) { + Pair pair = logicQueue.getEarliestUnitAndStoreTime(); + if (pair != null && pair.getObject2() != null) { + return pair.getObject2(); + } + } + + return -1; + } + + @Override + public CompletableFuture getEarliestMessageTimeAsync(String topic, int queueId) { + return CompletableFuture.completedFuture(getEarliestMessageTime(topic, queueId)); + } + + @Override + public long getEarliestMessageTime() { + long minPhyOffset = this.getMinPhyOffset(); + if (this.getCommitLog() instanceof DLedgerCommitLog) { + minPhyOffset += DLedgerEntry.BODY_OFFSET; + } + int size = MessageDecoder.MESSAGE_STORE_TIMESTAMP_POSITION + 8; + InetAddressValidator validator = InetAddressValidator.getInstance(); + if (validator.isValidInet6Address(this.brokerConfig.getBrokerIP1())) { + size = MessageDecoder.MESSAGE_STORE_TIMESTAMP_POSITION + 20; + } + return this.getCommitLog().pickupStoreTimestamp(minPhyOffset, size); + } + + @Override + public long getMessageStoreTimeStamp(String topic, int queueId, long consumeQueueOffset) { + ConsumeQueueInterface logicQueue = this.getConsumeQueue(topic, queueId); + if (logicQueue != null) { + 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) { + ConsumeQueueInterface logicQueue = this.getConsumeQueue(topic, queueId); + if (logicQueue != null) { + return logicQueue.getMessageTotalInQueue(); + } + + return 0; + } + + @Override + public SelectMappedBufferResult getCommitLogData(final long offset) { + if (this.shutdown) { + LOGGER.warn("message store has shutdown, so getPhyQueueData is forbidden"); + return null; + } + + return this.commitLog.getData(offset); + } + + @Override + 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) { + LOGGER.warn("message store has shutdown, so appendToCommitLog is forbidden"); + return false; + } + + boolean result = this.commitLog.appendData(startOffset, data, dataStart, dataLength); + if (result) { + this.reputMessageService.wakeup(); + } else { + LOGGER.error( + "DefaultMessageStore#appendToCommitLog: failed to append data to commitLog, physical offset={}, data " + + "length={}", startOffset, data.length); + } + + return result; + } + + @Override + public void executeDeleteFilesManually() { + this.cleanCommitLogService.executeDeleteFilesManually(); + } + + @Override + public QueryMessageResult queryMessage(String topic, String key, int maxNum, long begin, long end) { + QueryMessageResult queryMessageResult = new QueryMessageResult(); + + long lastQueryMsgTime = end; + + for (int i = 0; i < 3; i++) { + QueryOffsetResult queryOffsetResult = this.indexService.queryOffset(topic, key, maxNum, begin, lastQueryMsgTime); + if (queryOffsetResult.getPhyOffsets().isEmpty()) { + break; + } + + Collections.sort(queryOffsetResult.getPhyOffsets()); + + queryMessageResult.setIndexLastUpdatePhyoffset(queryOffsetResult.getIndexLastUpdatePhyoffset()); + queryMessageResult.setIndexLastUpdateTimestamp(queryOffsetResult.getIndexLastUpdateTimestamp()); + + for (int m = 0; m < queryOffsetResult.getPhyOffsets().size(); m++) { + long offset = queryOffsetResult.getPhyOffsets().get(m); + + try { + MessageExt msg = this.lookMessageByOffset(offset); + if (0 == m) { + lastQueryMsgTime = msg.getStoreTimestamp(); + } + + 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) { + LOGGER.error("queryMessage exception", e); + } + } + + if (queryMessageResult.getBufferTotalSize() > 0) { + break; + } + + if (lastQueryMsgTime < begin) { + break; + } + } + + 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) { + 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() { + 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 + 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. + */ + @Override + public int deleteTopics(final Set deleteTopics) { + if (deleteTopics == null || deleteTopics.isEmpty()) { + return 0; + } + + int deleteCount = 0; + for (String topic : deleteTopics) { + ConcurrentMap queueTable = this.consumeQueueStore.findConsumeQueueMap(topic); + + 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); + } + LOGGER.info("DeleteTopic: ConsumeQueue has been cleaned, topic={}, queueId={}", cq.getTopic(), cq.getQueueId()); + this.consumeQueueStore.removeTopicQueueTable(cq.getTopic(), cq.getQueueId()); + } + + // 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; + } + + @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(); + + this.consumeQueueStore.cleanExpired(minCommitLogOffset); + } + + public Map getMessageIds(final String topic, final int queueId, long minOffset, long maxOffset, + SocketAddress storeHost) { + Map messageIds = new HashMap<>(); + if (this.shutdown) { + return messageIds; + } + + ConsumeQueueInterface consumeQueue = findConsumeQueue(topic, queueId); + if (consumeQueue != null) { + minOffset = Math.max(minOffset, consumeQueue.getMinOffsetInQueue()); + maxOffset = Math.min(maxOffset, consumeQueue.getMaxOffsetInQueue()); + + if (maxOffset == 0) { + return messageIds; + } + + long nextOffset = minOffset; + while (nextOffset < maxOffset) { + 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, cqUnit.getQueueOffset()); + nextOffset = cqUnit.getQueueOffset() + cqUnit.getBatchNum(); + if (nextOffset >= maxOffset) { + return messageIds; + } + } + } else { + return messageIds; + } + } finally { + if (bufferConsumeQueue != null) { + bufferConsumeQueue.release(); + } + } + } + } + return messageIds; + } + + @Override + @Deprecated + public boolean checkInDiskByConsumeOffset(final String topic, final int queueId, long consumeOffset) { + + final long maxOffsetPy = this.commitLog.getMaxOffset(); + + ConsumeQueueInterface consumeQueue = getConsumeQueue(topic, queueId); + if (consumeQueue != null) { + CqUnit cqUnit = consumeQueue.get(consumeOffset); + + if (cqUnit != null) { + long offsetPy = cqUnit.getPos(); + return !estimateInMemByCommitOffset(offsetPy, maxOffsetPy); + } else { + return false; + } + } + return false; + } + + @Override + public boolean checkInMemByConsumeOffset(final String topic, final int queueId, long consumeOffset, int batchSize) { + ConsumeQueueInterface consumeQueue = getConsumeQueue(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(); + } + @Override + public long dispatchBehindMilliseconds() { + return this.reputMessageService.behindMs(); + } + + public long flushBehindBytes() { + if (this.messageStoreConfig.isTransientStorePoolEnable()) { + return this.commitLog.remainHowManyDataToCommit() + this.commitLog.remainHowManyDataToFlush(); + } else { + return 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) { + //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) { + try { + return MessageDecoder.decode(sbr.getByteBuffer(), true, false); + } finally { + sbr.release(); + } + } + + return null; + } + + @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()) { + nextOffset = newOffset; + } + return nextOffset; + } + + 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 unitBatchNum, int maxMsgNums, long maxMsgSize, int bufferTotal, + int messageTotal, boolean isInMem) { + + if (0 == bufferTotal || 0 == messageTotal) { + return false; + } + + if (messageTotal + unitBatchNum > maxMsgNums) { + return true; + } + + if (bufferTotal + sizePy > maxMsgSize) { + return true; + } + + if (isInMem) { + if ((bufferTotal + sizePy) > this.messageStoreConfig.getMaxTransferBytesOnMessageInMemory()) { + return true; + } + + return messageTotal > this.messageStoreConfig.getMaxTransferCountOnMessageInMemory() - 1; + } else { + if ((bufferTotal + sizePy) > this.messageStoreConfig.getMaxTransferBytesOnMessageInDisk()) { + return true; + } + + return messageTotal > this.messageStoreConfig.getMaxTransferCountOnMessageInDisk() - 1; + } + } + + private void deleteFile(final String fileName) { + File file = new File(fileName); + boolean result = file.delete(); + LOGGER.info(fileName + (result ? " delete OK" : " delete Failed")); + } + + /** + * @throws IOException + */ + private void createTempFile() throws IOException { + String fileName = StorePathConfigHelper.getAbortFile(this.messageStoreConfig.getStorePathRootDir()); + File file = new File(fileName); + UtilAll.ensureDirOK(file.getParent()); + boolean result = file.createNewFile(); + LOGGER.info(fileName + (result ? " create OK" : " already exists")); + MixAll.string2File(Long.toString(MixAll.getPID()), file.getAbsolutePath()); + } + + private void addScheduleTask() { + + this.scheduledExecutorService.scheduleAtFixedRate(new AbstractBrokerRunnable(this.getBrokerIdentity()) { + @Override + public void run0() { + DefaultMessageStore.this.cleanFilesPeriodically(); + } + }, 1000 * 60, this.messageStoreConfig.getCleanResourceInterval(), TimeUnit.MILLISECONDS); + + this.scheduledExecutorService.scheduleAtFixedRate(new AbstractBrokerRunnable(this.getBrokerIdentity()) { + @Override + public void run0() { + DefaultMessageStore.this.checkSelf(); + } + }, 1, 10, TimeUnit.MINUTES); + + this.scheduledExecutorService.scheduleAtFixedRate(new AbstractBrokerRunnable(this.getBrokerIdentity()) { + @Override + public void run0() { + if (DefaultMessageStore.this.getMessageStoreConfig().isDebugLockEnable()) { + try { + if (DefaultMessageStore.this.commitLog.getBeginTimeInLock() != 0) { + long lockTime = System.currentTimeMillis() - DefaultMessageStore.this.commitLog.getBeginTimeInLock(); + if (lockTime > 1000 && lockTime < 10000000) { + + String stack = UtilAll.jstack(); + final String fileName = System.getProperty("user.home") + File.separator + "debug/lock/stack-" + + DefaultMessageStore.this.commitLog.getBeginTimeInLock() + "-" + lockTime; + MixAll.string2FileNotSafe(stack, fileName); + } + } + } catch (Exception e) { + } + } + } + }, 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() { + // DefaultMessageStore.this.cleanExpiredConsumerQueue(); + // } + // }, 1, 1, TimeUnit.HOURS); + } + + private void cleanFilesPeriodically() { + this.cleanCommitLogService.run(); + } + + private void cleanQueueFilesPeriodically() { + this.correctLogicOffsetService.run(); + this.cleanConsumeQueueService.run(); + } + + private void checkSelf() { + this.commitLog.checkSelf(); + this.consumeQueueStore.checkSelf(); + } + + private boolean isTempFileExist() { + String fileName = StorePathConfigHelper.getAbortFile(this.messageStoreConfig.getStorePathRootDir()); + File file = new File(fileName); + return file.exists(); + } + + private boolean isRecoverConcurrently() { + return this.brokerConfig.isRecoverConcurrently() && !this.messageStoreConfig.isEnableRocksDBStore(); + } + + 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(maxPhyOffsetOfConsumeQueue); + } else { + 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; + } + + @Override + public TransientStorePool getTransientStorePool() { + return transientStorePool; + } + + private void recoverConsumeQueue() { + if (!this.isRecoverConcurrently()) { + this.consumeQueueStore.recover(); + } else { + this.consumeQueueStore.recoverConcurrently(); + } + } + + @Override + public void recoverTopicQueueTable() { + long minPhyOffset = this.commitLog.getMinOffset(); + this.consumeQueueStore.recoverOffsetTable(minPhyOffset); + } + + @Override + public AllocateMappedFileService getAllocateMappedFileService() { + return allocateMappedFileService; + } + + @Override + public StoreStatsService getStoreStatsService() { + return storeStatsService; + } + + public RunningFlags getAccessRights() { + return runningFlags; + } + + public ConcurrentMap> getConsumeQueueTable() { + return consumeQueueStore.getConsumeQueueTable(); + } + + @Override + public StoreCheckpoint getStoreCheckpoint() { + return storeCheckpoint; + } + + @Override + public HAService getHaService() { + return haService; + } + + @Override + public RunningFlags getRunningFlags() { + return runningFlags; + } + + public void doDispatch(DispatchRequest req) throws RocksDBException { + for (CommitLogDispatcher dispatcher : this.dispatcherList) { + dispatcher.dispatch(req); + } + } + + /** + * @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() { + if (this.isTransientStorePoolEnable()) { + return this.transientStorePool.availableBufferNums(); + } + return Integer.MAX_VALUE; + } + + @Override + 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 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 + public void run() { + mappedFile.munlock(); + } + }, 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) throws RocksDBException { + final int tranType = MessageSysFlag.getTransactionValue(request.getSysFlag()); + switch (tranType) { + case MessageSysFlag.TRANSACTION_NOT_TYPE: + case MessageSysFlag.TRANSACTION_COMMIT_TYPE: + putMessagePositionInfo(request); + break; + case MessageSysFlag.TRANSACTION_PREPARED_TYPE: + case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE: + break; + } + } + } + + class CommitLogDispatcherBuildIndex implements CommitLogDispatcher { + + @Override + public void dispatch(DispatchRequest request) { + if (DefaultMessageStore.this.messageStoreConfig.isMessageIndexEnable()) { + DefaultMessageStore.this.indexService.buildIndex(request); + } + } + } + + class CleanCommitLogService { + + private final static int MAX_MANUAL_DELETE_FILE_TIMES = 20; + private final String diskSpaceWarningLevelRatio = + System.getProperty("rocketmq.broker.diskSpaceWarningLevelRatio", ""); + + private final String diskSpaceCleanForciblyRatio = + System.getProperty("rocketmq.broker.diskSpaceCleanForciblyRatio", ""); + private long lastRedeleteTimestamp = 0; + + private final AtomicInteger manualDeleteFileSeveralTimes = new AtomicInteger(); + + private volatile boolean cleanImmediately = false; + + 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.set(MAX_MANUAL_DELETE_FILE_TIMES); + DefaultMessageStore.LOGGER.info("executeDeleteFilesManually was invoked"); + } + + public void run() { + try { + this.deleteExpiredFiles(); + this.reDeleteHangedFile(); + } catch (Throwable e) { + DefaultMessageStore.LOGGER.warn(this.getServiceName() + " service has exception. ", e); + } + } + + private void deleteExpiredFiles() { + int deleteCount = 0; + long fileReservedTime = DefaultMessageStore.this.getMessageStoreConfig().getFileReservedTime(); + int deletePhysicFilesInterval = DefaultMessageStore.this.getMessageStoreConfig().getDeleteCommitLogFilesInterval(); + int destroyMappedFileIntervalForcibly = DefaultMessageStore.this.getMessageStoreConfig().getDestroyMapedFileIntervalForcibly(); + int deleteFileBatchMax = DefaultMessageStore.this.getMessageStoreConfig().getDeleteFileBatchMax(); + + boolean isTimeUp = this.isTimeToDelete(); + boolean isUsageExceedsThreshold = this.isSpaceToDelete(); + boolean isManualDelete = this.manualDeleteFileSeveralTimes.get() > 0; + + if (isTimeUp || isUsageExceedsThreshold || isManualDelete) { + + if (isManualDelete) { + this.manualDeleteFileSeveralTimes.decrementAndGet(); + } + + boolean cleanAtOnce = DefaultMessageStore.this.getMessageStoreConfig().isCleanFileForciblyEnable() && this.cleanImmediately; + + LOGGER.info("begin to delete before {} hours file. isTimeUp: {} isUsageExceedsThreshold: {} manualDeleteFileSeveralTimes: {} cleanAtOnce: {} deleteFileBatchMax: {}", + fileReservedTime, + isTimeUp, + isUsageExceedsThreshold, + manualDeleteFileSeveralTimes.get(), + cleanAtOnce, + deleteFileBatchMax); + + fileReservedTime *= 60 * 60 * 1000; + + deleteCount = DefaultMessageStore.this.commitLog.deleteExpiredFile(fileReservedTime, deletePhysicFilesInterval, + destroyMappedFileIntervalForcibly, cleanAtOnce, deleteFileBatchMax); + if (deleteCount > 0) { + // 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() { + int interval = DefaultMessageStore.this.getMessageStoreConfig().getRedeleteHangedFileInterval(); + long currentTimestamp = System.currentTimeMillis(); + if ((currentTimestamp - this.lastRedeleteTimestamp) > interval) { + this.lastRedeleteTimestamp = currentTimestamp; + int destroyMappedFileIntervalForcibly = + DefaultMessageStore.this.getMessageStoreConfig().getDestroyMapedFileIntervalForcibly(); + if (DefaultMessageStore.this.commitLog.retryDeleteFirstFile(destroyMappedFileIntervalForcibly)) { + } + } + } + + public String getServiceName() { + return DefaultMessageStore.this.brokerConfig.getIdentifier() + CleanCommitLogService.class.getSimpleName(); + } + + protected boolean isTimeToDelete() { + String when = DefaultMessageStore.this.getMessageStoreConfig().getDeleteWhen(); + if (UtilAll.isItTimeToDo(when)) { + DefaultMessageStore.LOGGER.info("it's time to reclaim disk space, " + when); + return true; + } + + return false; + } + + private boolean isSpaceToDelete() { + cleanImmediately = false; + + 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 (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); + } + + 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 > getDiskSpaceWarningLevelRatio()) { + boolean diskOK = DefaultMessageStore.this.runningFlags.getAndMakeDiskFull(); + if (diskOK) { + DefaultMessageStore.LOGGER.error("logics disk maybe full soon " + logicsRatio + ", so mark disk full"); + } + + 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.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; + } + + boolean isUsageExceedsThreshold = logicalRatio > ratio; + if (isUsageExceedsThreshold) { + DefaultMessageStore.LOGGER.info("Logical disk usage {} exceeds clean threshold {}, forcibly: {}", + logicalRatio, ratio, cleanImmediately); + } + return isUsageExceedsThreshold; + } + } + + public int getManualDeleteFileSeveralTimes() { + return manualDeleteFileSeveralTimes.get(); + } + + public void setManualDeleteFileSeveralTimes(int manualDeleteFileSeveralTimes) { + this.manualDeleteFileSeveralTimes.set(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 { + protected long lastPhysicalMinOffset = 0; + + public void run() { + try { + this.deleteExpiredFiles(); + } catch (Throwable e) { + DefaultMessageStore.LOGGER.warn(this.getServiceName() + " service has exception. ", e); + } + } + + 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.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); + } catch (InterruptedException ignored) { + } + } + } + } + + DefaultMessageStore.this.indexService.deleteExpiredFile(minOffset); + } + } + + public String getServiceName() { + 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(); + } + } + + class FlushConsumeQueueService extends ServiceThread { + private static final int RETRY_TIMES_OVER = 3; + private long lastFlushTimestamp = 0; + + private void doFlush(int retryTimes) { + int flushConsumeQueueLeastPages = DefaultMessageStore.this.getMessageStoreConfig().getFlushConsumeQueueLeastPages(); + + if (retryTimes == RETRY_TIMES_OVER) { + flushConsumeQueueLeastPages = 0; + } + + long logicsMsgTimestamp = 0; + + int flushConsumeQueueThoroughInterval = DefaultMessageStore.this.getMessageStoreConfig().getFlushConsumeQueueThoroughInterval(); + long currentTimeMillis = System.currentTimeMillis(); + if (currentTimeMillis >= (this.lastFlushTimestamp + flushConsumeQueueThoroughInterval)) { + this.lastFlushTimestamp = currentTimeMillis; + flushConsumeQueueLeastPages = 0; + logicsMsgTimestamp = DefaultMessageStore.this.getStoreCheckpoint().getLogicsMsgTimestamp(); + } + + ConcurrentMap> tables = DefaultMessageStore.this.getConsumeQueueTable(); + + for (ConcurrentMap maps : tables.values()) { + for (ConsumeQueueInterface cq : maps.values()) { + boolean result = false; + for (int i = 0; i < retryTimes && !result; i++) { + result = DefaultMessageStore.this.consumeQueueStore.flush(cq, flushConsumeQueueLeastPages); + } + } + } + + if (messageStoreConfig.isEnableCompaction()) { + compactionStore.flush(flushConsumeQueueLeastPages); + } + + if (0 == flushConsumeQueueLeastPages) { + if (logicsMsgTimestamp > 0) { + DefaultMessageStore.this.getStoreCheckpoint().setLogicsMsgTimestamp(logicsMsgTimestamp); + } + DefaultMessageStore.this.getStoreCheckpoint().flush(); + } + } + + @Override + public void run() { + DefaultMessageStore.LOGGER.info(this.getServiceName() + " service started"); + + while (!this.isStopped()) { + try { + int interval = DefaultMessageStore.this.getMessageStoreConfig().getFlushIntervalConsumeQueue(); + this.waitForRunning(interval); + this.doFlush(1); + } catch (Exception e) { + DefaultMessageStore.LOGGER.warn(this.getServiceName() + " service has exception. ", e); + } + } + + this.doFlush(RETRY_TIMES_OVER); + + 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() { + return 1000 * 60; + } + } + + static class BatchDispatchRequest { + + private final ByteBuffer byteBuffer; + + private final int position; + + private final int size; + + private final long id; + + public BatchDispatchRequest(ByteBuffer byteBuffer, int position, int size, long id) { + this.byteBuffer = byteBuffer; + this.position = position; + this.size = size; + this.id = id; + } + } + + static 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 { + + protected volatile long reputFromOffset = 0; + protected volatile long currentReputTimestamp = System.currentTimeMillis(); + + public long getReputFromOffset() { + return reputFromOffset; + } + + public void setReputFromOffset(long reputFromOffset) { + this.reputFromOffset = reputFromOffset; + } + + public long getCurrentReputTimestamp() { + return currentReputTimestamp; + } + + @Override + public void shutdown() { + for (int i = 0; i < 50 && this.isCommitLogAvailable(); i++) { + try { + Thread.sleep(100); + } catch (InterruptedException ignored) { + } + } + + if (this.isCommitLogAvailable()) { + 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.getConfirmOffset() - this.reputFromOffset; + } + + public long behindMs() { + long lastCommitLogFileTimeStamp = System.currentTimeMillis(); + MappedFile lastMappedFile = DefaultMessageStore.this.commitLog.getMappedFileQueue().getLastMappedFile(); + if (lastMappedFile != null) { + lastCommitLogFileTimeStamp = lastMappedFile.getStoreTimestamp(); + } + return Math.max(0, lastCommitLogFileTimeStamp - this.currentReputTimestamp); + } + + public boolean isCommitLogAvailable() { + return this.reputFromOffset < getReputEndOffset(); + } + + protected long getReputEndOffset() { + return DefaultMessageStore.this.getMessageStoreConfig().isReadUnCommitted() ? DefaultMessageStore.this.commitLog.getMaxOffset() : DefaultMessageStore.this.commitLog.getConfirmOffset(); + } + + 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(); + } + boolean isCommitLogAvailable = isCommitLogAvailable(); + if (!isCommitLogAvailable) { + currentReputTimestamp = System.currentTimeMillis(); + } + for (boolean doNext = true; isCommitLogAvailable && doNext; ) { + + SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset); + + if (result == null) { + break; + } + + try { + this.reputFromOffset = result.getStartOffset(); + + for (int readSize = 0; readSize < result.getSize() && reputFromOffset < getReputEndOffset() && doNext; ) { + DispatchRequest dispatchRequest = + DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false, false); + int size = dispatchRequest.getBufferSize() == -1 ? dispatchRequest.getMsgSize() : dispatchRequest.getBufferSize(); + + if (reputFromOffset + size > getReputEndOffset()) { + doNext = false; + break; + } + + if (dispatchRequest.isSuccess()) { + if (size > 0) { + currentReputTimestamp = dispatchRequest.getStoreTimestamp(); + 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; + } + } + } + } + } catch (RocksDBException e) { + ERROR_LOG.info("dispatch message to cq exception. reputFromOffset: {}", this.reputFromOffset, e); + return; + } finally { + result.release(); + } + } + } + + 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.LMQ_DISPATCH_SEPARATOR); + String[] queueOffsets = multiQueueOffset.split(MixAll.LMQ_DISPATCH_SEPARATOR); + 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 = MixAll.LMQ_QUEUE_ID; + } + DefaultMessageStore.this.messageArrivingListener.arriving( + queueName, queueId, queueOffset + 1, dispatchRequest.getTagsCode(), + dispatchRequest.getStoreTimestamp(), dispatchRequest.getBitMap(), dispatchRequest.getPropertiesMap()); + } + } + + @Override + public void run() { + DefaultMessageStore.LOGGER.info(this.getServiceName() + " service started"); + + while (!this.isStopped()) { + try { + TimeUnit.MILLISECONDS.sleep(1); + this.doReput(); + } catch (Throwable 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() + 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 < getReputEndOffset() && 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(); + } + } + } + + /** + * 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 void enableRocksdbCQWrite() { + try { + RocksDBMessageStore store = new RocksDBMessageStore(this.messageStoreConfig, this.brokerStatsManager, this.messageArrivingListener, this.brokerConfig, this.topicConfigTable); + this.rocksDBMessageStore = store; + store.loadAndStartConsumerServiceOnly(); + addDispatcher(store.getDispatcherBuildRocksdbConsumeQueue()); + } catch (Exception e) { + LOGGER.error("enableRocksdbCqWrite error", e); + } + } + + 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(); + } + + public RocksDBMessageStore getRocksDBMessageStore() { + return this.rocksDBMessageStore; + } + + public ConsumeQueueStoreInterface getConsumeQueueStore() { + return consumeQueueStore; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/DispatchRequest.java b/store/src/main/java/org/apache/rocketmq/store/DispatchRequest.java new file mode 100644 index 0000000..654760b --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/DispatchRequest.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.store; + +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.message.MessageConst; + +public class DispatchRequest { + private final String topic; + private final int queueId; + private final long commitLogOffset; + private int msgSize; + private final long tagsCode; + private final long storeTimestamp; + private final long consumeQueueOffset; + private final String keys; + private final boolean success; + private final String uniqKey; + + private final int sysFlag; + private final long preparedTransactionOffset; + 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, + final long commitLogOffset, + final int msgSize, + final long tagsCode, + final long storeTimestamp, + final long consumeQueueOffset, + final String keys, + final String uniqKey, + final int sysFlag, + final long preparedTransactionOffset, + final Map propertiesMap + ) { + this.topic = topic; + this.queueId = queueId; + this.commitLogOffset = commitLogOffset; + this.msgSize = msgSize; + this.tagsCode = tagsCode; + this.storeTimestamp = storeTimestamp; + this.consumeQueueOffset = consumeQueueOffset; + this.msgBaseOffset = consumeQueueOffset; + this.keys = keys; + this.uniqKey = uniqKey; + + this.sysFlag = sysFlag; + this.preparedTransactionOffset = preparedTransactionOffset; + this.success = true; + 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; + this.commitLogOffset = 0; + this.msgSize = size; + this.tagsCode = 0; + this.storeTimestamp = 0; + this.consumeQueueOffset = 0; + this.keys = ""; + this.uniqKey = null; + this.sysFlag = 0; + this.preparedTransactionOffset = 0; + this.success = false; + this.propertiesMap = null; + } + + public DispatchRequest(int size, boolean success) { + this.topic = ""; + this.queueId = 0; + this.commitLogOffset = 0; + this.msgSize = size; + this.tagsCode = 0; + this.storeTimestamp = 0; + this.consumeQueueOffset = 0; + this.keys = ""; + this.uniqKey = null; + this.sysFlag = 0; + this.preparedTransactionOffset = 0; + this.success = success; + this.propertiesMap = null; + } + + public String getTopic() { + return topic; + } + + public int getQueueId() { + return queueId; + } + + public long getCommitLogOffset() { + return commitLogOffset; + } + + public int getMsgSize() { + return msgSize; + } + + public long getStoreTimestamp() { + return storeTimestamp; + } + + public long getConsumeQueueOffset() { + return consumeQueueOffset; + } + + public String getKeys() { + return keys; + } + + public long getTagsCode() { + return tagsCode; + } + + public int getSysFlag() { + return sysFlag; + } + + public long getPreparedTransactionOffset() { + return preparedTransactionOffset; + } + + public boolean isSuccess() { + return success; + } + + public String getUniqKey() { + return uniqKey; + } + + public Map getPropertiesMap() { + return propertiesMap; + } + + public byte[] getBitMap() { + return bitMap; + } + + 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; + } + + public boolean containsLMQ() { + if (!MixAll.topicAllowsLMQ(topic)) { + return false; + } + if (null == propertiesMap || propertiesMap.isEmpty()) { + return false; + } + String lmqNames = propertiesMap.get(MessageConst.PROPERTY_INNER_MULTI_DISPATCH); + String lmqOffsets = propertiesMap.get(MessageConst.PROPERTY_INNER_MULTI_QUEUE_OFFSET); + return !StringUtils.isBlank(lmqNames) && !StringUtils.isBlank(lmqOffsets); + } + + @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 0000000..6521a76 --- /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 0000000..a75efd6 --- /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 0000000..fe3951a --- /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 new file mode 100644 index 0000000..6f322a1 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/GetMessageResult.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.store; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class GetMessageResult { + + private final List messageMapedList; + private final List messageBufferList; + private final List messageQueueOffset; + + private GetMessageStatus status; + private long nextBeginOffset; + private long minOffset; + private long maxOffset; + + 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; + + private int filterMessageCount; + + 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() { + return status; + } + + public void setStatus(GetMessageStatus status) { + this.status = status; + } + + 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 List getMessageMapedList() { + return messageMapedList; + } + + public List getMessageBufferList() { + return messageBufferList; + } + + public void addMessage(final SelectMappedBufferResult mapedBuffer) { + this.messageMapedList.add(mapedBuffer); + this.messageBufferList.add(mapedBuffer.getByteBuffer()); + this.bufferTotalSize += mapedBuffer.getSize(); + this.msgCount4Commercial += (int) Math.ceil( + 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() { + for (SelectMappedBufferResult select : this.messageMapedList) { + select.release(); + } + } + + public int getBufferTotalSize() { + return bufferTotalSize; + } + + public int getMessageCount() { + return messageCount; + } + + public boolean isSuggestPullingFromSlave() { + return suggestPullingFromSlave; + } + + public void setSuggestPullingFromSlave(boolean suggestPullingFromSlave) { + this.suggestPullingFromSlave = suggestPullingFromSlave; + } + + public int getMsgCount4Commercial() { + return msgCount4Commercial; + } + + 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; + } + + public int getFilterMessageCount() { + return filterMessageCount; + } + + public void setFilterMessageCount(int filterMessageCount) { + this.filterMessageCount = filterMessageCount; + } + + @Override + public String toString() { + return "GetMessageResult [status=" + status + ", nextBeginOffset=" + nextBeginOffset + ", minOffset=" + + minOffset + ", maxOffset=" + maxOffset + ", bufferTotalSize=" + bufferTotalSize + ", messageCount=" + messageCount + + ", filterMessageCount=" + filterMessageCount + ", 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 new file mode 100644 index 0000000..bc24486 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/GetMessageStatus.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.store; + +public enum GetMessageStatus { + + FOUND, + + NO_MATCHED_MESSAGE, + + MESSAGE_WAS_REMOVING, + + OFFSET_FOUND_NULL, + + OFFSET_OVERFLOW_BADLY, + + OFFSET_OVERFLOW_ONE, + + OFFSET_TOO_SMALL, + + NO_MATCHED_LOGIC_QUEUE, + + NO_MESSAGE_IN_QUEUE, + + OFFSET_RESET +} diff --git a/store/src/main/java/org/apache/rocketmq/store/LmqDispatch.java b/store/src/main/java/org/apache/rocketmq/store/LmqDispatch.java new file mode 100644 index 0000000..2805f51 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/LmqDispatch.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.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; +import org.apache.rocketmq.store.exception.ConsumeQueueException; + +public class LmqDispatch { + private static final short VALUE_OF_EACH_INCREMENT = 1; + + public static void wrapLmqDispatch(MessageStore messageStore, final MessageExtBrokerInner msg) + throws ConsumeQueueException { + String lmqNames = msg.getProperty(MessageConst.PROPERTY_INNER_MULTI_DISPATCH); + String[] queueNames = lmqNames.split(MixAll.LMQ_DISPATCH_SEPARATOR); + Long[] queueOffsets = new Long[queueNames.length]; + if (messageStore.getMessageStoreConfig().isEnableLmq()) { + for (int i = 0; i < queueNames.length; i++) { + if (MixAll.isLmq(queueNames[i])) { + queueOffsets[i] = messageStore.getQueueStore().getLmqQueueOffset(queueNames[i], MixAll.LMQ_QUEUE_ID); + } + } + } + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_INNER_MULTI_QUEUE_OFFSET, + StringUtils.join(queueOffsets, MixAll.LMQ_DISPATCH_SEPARATOR)); + msg.removeWaitStorePropertyString(); + } + + public static void updateLmqOffsets(MessageStore messageStore, final MessageExtBrokerInner msgInner) + throws ConsumeQueueException { + String lmqNames = msgInner.getProperty(MessageConst.PROPERTY_INNER_MULTI_DISPATCH); + String[] queueNames = lmqNames.split(MixAll.LMQ_DISPATCH_SEPARATOR); + for (String queueName : queueNames) { + if (messageStore.getMessageStoreConfig().isEnableLmq() && MixAll.isLmq(queueName)) { + messageStore.getQueueStore().increaseLmqOffset(queueName, MixAll.LMQ_QUEUE_ID, VALUE_OF_EACH_INCREMENT); + } + } + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/MappedFileQueue.java b/store/src/main/java/org/apache/rocketmq/store/MappedFileQueue.java new file mode 100644 index 0000000..e32c16a --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/MappedFileQueue.java @@ -0,0 +1,917 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.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 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); + + protected final String storePath; + + protected final int mappedFileSize; + + protected final CopyOnWriteArrayList mappedFiles = new CopyOnWriteArrayList<>(); + + protected final AllocateMappedFileService allocateMappedFileService; + + protected long flushedWhere = 0; + protected long committedWhere = 0; + + protected volatile long storeTimestamp = 0; + + public MappedFileQueue(final String storePath, int mappedFileSize, + AllocateMappedFileService allocateMappedFileService) { + this.storePath = storePath; + this.mappedFileSize = mappedFileSize; + this.allocateMappedFileService = allocateMappedFileService; + } + + public void checkSelf() { + List mappedFiles = new ArrayList<>(this.mappedFiles); + if (!mappedFiles.isEmpty()) { + Iterator iterator = mappedFiles.iterator(); + MappedFile pre = null; + while (iterator.hasNext()) { + MappedFile cur = iterator.next(); + + if (pre != null) { + if (cur.getFileFromOffset() - pre.getFileFromOffset() != this.mappedFileSize) { + LOG_ERROR.error("[BUG]The mappedFile queue's data is damaged, the adjacent mappedFile's offset don't match. pre file {}, cur file {}", + pre.getFileName(), cur.getFileName()); + } + } + pre = cur; + } + } + } + + 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); + + if (null == mfs) + return null; + + for (int i = 0; i < mfs.length; i++) { + MappedFile mappedFile = (MappedFile) mfs[i]; + if (mappedFile.getLastModifiedTimestamp() >= timestamp) { + return mappedFile; + } + } + + return (MappedFile) mfs[mfs.length - 1]; + } + + protected Object[] copyMappedFiles(final int reservedMappedFiles) { + Object[] mfs; + + if (this.mappedFiles.size() <= reservedMappedFiles) { + return null; + } + + mfs = this.mappedFiles.toArray(); + return mfs; + } + + public void truncateDirtyFiles(long offset) { + List willRemoveFiles = new ArrayList<>(); + + for (MappedFile file : this.mappedFiles) { + long fileTailOffset = file.getFileFromOffset() + this.mappedFileSize; + if (fileTailOffset > offset) { + if (offset >= file.getFileFromOffset()) { + file.setWrotePosition((int) (offset % this.mappedFileSize)); + file.setCommittedPosition((int) (offset % this.mappedFileSize)); + file.setFlushedPosition((int) (offset % this.mappedFileSize)); + } else { + file.destroy(1000); + willRemoveFiles.add(file); + } + } + } + + this.deleteExpiredFile(willRemoveFiles); + } + + void deleteExpiredFile(List files) { + + if (!files.isEmpty()) { + + Iterator iterator = files.iterator(); + while (iterator.hasNext()) { + MappedFile cur = iterator.next(); + if (!this.mappedFiles.contains(cur)) { + iterator.remove(); + log.info("This mappedFile {} is not contained by mappedFiles, so skip it.", cur.getFileName()); + } + } + + try { + if (!this.mappedFiles.removeAll(files)) { + log.error("deleteExpiredFile remove failed."); + } + } catch (Exception e) { + log.error("deleteExpiredFile has exception.", e); + } + } + } + + + public boolean load() { + File dir = new File(this.storePath); + File[] ls = dir.listFiles(); + if (ls != null) { + return doLoad(Arrays.asList(ls)); + } + return true; + } + + 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; + } + + public long howMuchFallBehind() { + if (this.mappedFiles.isEmpty()) + return 0; + + long committed = this.getFlushedWhere(); + if (committed != 0) { + MappedFile mappedFile = this.getLastMappedFile(0, false); + if (mappedFile != null) { + return (mappedFile.getFileFromOffset() + mappedFile.getWrotePosition()) - committed; + } + } + + return 0; + } + + public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) { + long createOffset = -1; + MappedFile mappedFileLast = getLastMappedFile(); + + if (mappedFileLast == null) { + createOffset = startOffset - (startOffset % this.mappedFileSize); + } + + if (mappedFileLast != null && mappedFileLast.isFull()) { + createOffset = mappedFileLast.getFileFromOffset() + this.mappedFileSize; + } + + if (createOffset != -1 && needCreate) { + 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); + } + + 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 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); + } + + return mappedFile; + } + + public MappedFile getLastMappedFile(final long startOffset) { + return getLastMappedFile(startOffset, true); + } + + public MappedFile getLastMappedFile() { + MappedFile mappedFileLast = null; + while (!this.mappedFiles.isEmpty()) { + try { + mappedFileLast = this.mappedFiles.get(this.mappedFiles.size() - 1); + break; + } catch (IndexOutOfBoundsException e) { + //continue; + } catch (Exception e) { + log.error("getLastMappedFile has exception.", e); + break; + } + } + return mappedFileLast; + } + + public boolean resetOffset(long offset) { + MappedFile mappedFileLast = getLastMappedFile(); + + if (mappedFileLast != null) { + long lastOffset = mappedFileLast.getFileFromOffset() + + mappedFileLast.getWrotePosition(); + long diff = lastOffset - offset; + + final int maxDiff = this.mappedFileSize * 2; + if (diff > maxDiff) + return false; + } + + ListIterator iterator = this.mappedFiles.listIterator(mappedFiles.size()); + List toRemoves = new ArrayList<>(); + + while (iterator.hasPrevious()) { + mappedFileLast = iterator.previous(); + if (offset >= mappedFileLast.getFileFromOffset()) { + int where = (int) (offset % mappedFileLast.getFileSize()); + mappedFileLast.setFlushedPosition(where); + mappedFileLast.setWrotePosition(where); + mappedFileLast.setCommittedPosition(where); + break; + } else { + toRemoves.add(mappedFileLast); + } + } + + if (!toRemoves.isEmpty()) { + this.mappedFiles.removeAll(toRemoves); + } + + return true; + } + + public long getMinOffset() { + + if (!this.mappedFiles.isEmpty()) { + try { + return this.mappedFiles.get(0).getFileFromOffset(); + } catch (IndexOutOfBoundsException e) { + //continue; + } catch (Exception e) { + log.error("getMinOffset has exception.", e); + } + } + return -1; + } + + public long getMaxOffset() { + MappedFile mappedFile = getLastMappedFile(); + if (mappedFile != null) { + return mappedFile.getFileFromOffset() + mappedFile.getReadPosition(); + } + return 0; + } + + public long getMaxWrotePosition() { + MappedFile mappedFile = getLastMappedFile(); + if (mappedFile != null) { + return mappedFile.getFileFromOffset() + mappedFile.getWrotePosition(); + } + return 0; + } + + public long remainHowManyDataToCommit() { + return getMaxWrotePosition() - getCommittedWhere(); + } + + public long remainHowManyDataToFlush() { + return getMaxOffset() - this.getFlushedWhere(); + } + + public void deleteLastMappedFile() { + MappedFile lastMappedFile = getLastMappedFile(); + if (lastMappedFile != null) { + lastMappedFile.destroy(1000); + this.mappedFiles.remove(lastMappedFile); + log.info("on recover, destroy a logic mapped file " + lastMappedFile.getFileName()); + + } + } + + public int deleteExpiredFileByTime(final long expiredTime, + final int deleteFilesInterval, + final long intervalForcibly, + final boolean cleanImmediately, + final int deleteFileBatchMax) { + Object[] mfs = this.copyMappedFiles(0); + + if (null == mfs) + return 0; + + int mfsLength = mfs.length - 1; + int deleteCount = 0; + 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() >= deleteFileBatchMax) { + break; + } + + if (deleteFilesInterval > 0 && (i + 1) < mfsLength) { + try { + Thread.sleep(deleteFilesInterval); + } catch (InterruptedException e) { + } + } + } else { + break; + } + } else { + skipFileNum++; + //avoid deleting files in the middle + break; + } + } + } + + deleteExpiredFile(files); + + return deleteCount; + } + + public int deleteExpiredFileByOffset(long offset, 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; + MappedFile mappedFile = (MappedFile) mfs[i]; + SelectMappedBufferResult result = mappedFile.selectMappedBuffer(this.mappedFileSize - unitSize); + if (result != null) { + long maxOffsetInLogicQueue = result.getByteBuffer().getLong(); + result.release(); + destroy = maxOffsetInLogicQueue < offset; + if (destroy) { + log.info("physic min offset " + offset + ", logics in current mappedFile max offset " + + maxOffsetInLogicQueue + ", delete it"); + } + } 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; + } + + if (destroy && mappedFile.destroy(1000 * 60)) { + files.add(mappedFile); + deleteCount++; + } else { + break; + } + } + } + + deleteExpiredFile(files); + + 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.getFlushedWhere(), this.getFlushedWhere() == 0); + if (mappedFile != null) { + long tmpTimeStamp = mappedFile.getStoreTimestamp(); + int offset = mappedFile.flush(flushLeastPages); + long where = mappedFile.getFileFromOffset() + offset; + result = where == this.getFlushedWhere(); + this.setFlushedWhere(where); + if (0 == flushLeastPages) { + this.setStoreTimestamp(tmpTimeStamp); + } + } + + return result; + } + + public synchronized boolean commit(final int commitLeastPages) { + boolean result = true; + 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.getCommittedWhere(); + this.setCommittedWhere(where); + } + + return result; + } + + /** + * Finds a mapped file by offset. + * + * @param offset Offset. + * @param returnFirstOnNotFound If the mapped file is not found, then return the first one. + * @return Mapped file or null (when not found and returnFirstOnNotFound is false). + */ + public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound) { + try { + 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, + 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) { + } + + if (targetFile != null && offset >= targetFile.getFileFromOffset() + && offset < targetFile.getFileFromOffset() + this.mappedFileSize) { + return targetFile; + } + + for (MappedFile tmpMappedFile : this.mappedFiles) { + if (offset >= tmpMappedFile.getFileFromOffset() + && offset < tmpMappedFile.getFileFromOffset() + this.mappedFileSize) { + return tmpMappedFile; + } + } + } + + if (returnFirstOnNotFound) { + return firstMappedFile; + } + } + } catch (Exception e) { + log.error("findMappedFileByOffset Exception", e); + } + + return null; + } + + public MappedFile getFirstMappedFile() { + MappedFile mappedFileFirst = null; + + if (!this.mappedFiles.isEmpty()) { + try { + mappedFileFirst = this.mappedFiles.get(0); + } catch (IndexOutOfBoundsException e) { + //ignore + } catch (Exception e) { + log.error("getFirstMappedFile has exception.", e); + } + } + + return mappedFileFirst; + } + + public MappedFile findMappedFileByOffset(final long offset) { + return findMappedFileByOffset(offset, false); + } + + public long getMappedMemorySize() { + long size = 0; + + Object[] mfs = this.copyMappedFiles(0); + if (mfs != null) { + for (Object mf : mfs) { + if (((ReferenceResource) mf).isAvailable()) { + size += this.mappedFileSize; + } + } + } + + return size; + } + + public boolean retryDeleteFirstFile(final long intervalForcibly) { + MappedFile mappedFile = this.getFirstMappedFile(); + if (mappedFile != null) { + if (!mappedFile.isAvailable()) { + log.warn("the mappedFile was destroyed once, but still alive, " + mappedFile.getFileName()); + boolean result = mappedFile.destroy(intervalForcibly); + if (result) { + log.info("the mappedFile re delete OK, " + mappedFile.getFileName()); + List tmpFiles = new ArrayList<>(); + tmpFiles.add(mappedFile); + this.deleteExpiredFile(tmpFiles); + } else { + log.warn("the mappedFile re delete failed, " + mappedFile.getFileName()); + } + + return result; + } + } + + return false; + } + + public void shutdown(final long intervalForcibly) { + for (MappedFile mf : this.mappedFiles) { + mf.shutdown(intervalForcibly); + } + } + + public void destroy() { + for (MappedFile mf : this.mappedFiles) { + mf.destroy(1000 * 3); + } + this.mappedFiles.clear(); + this.setFlushedWhere(0); + + // delete parent directory + File file = new File(storePath); + if (file.isDirectory()) { + file.delete(); + } + } + + @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; + } + + public void setFlushedWhere(long flushedWhere) { + this.flushedWhere = flushedWhere; + } + + public long getStoreTimestamp() { + return storeTimestamp; + } + + public void setStoreTimestamp(long storeTimestamp) { + this.storeTimestamp = storeTimestamp; + } + + public List getMappedFiles() { + return mappedFiles; + } + + public int getMappedFileSize() { + return mappedFileSize; + } + + public long getCommittedWhere() { + return committedWhere; + } + + 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 new file mode 100644 index 0000000..ceca98f --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/MessageArrivingListener.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.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/MessageExtEncoder.java b/store/src/main/java/org/apache/rocketmq/store/MessageExtEncoder.java new file mode 100644 index 0000000..500b0e6 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/MessageExtEncoder.java @@ -0,0 +1,419 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.isEnableLmq() && msgInner.needDispatchLMQ()) { + 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"); + } + + + 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); + + final byte[] topicData = messageExtBatch.getTopic().getBytes(MessageDecoder.CHARSET_UTF8); + + final int topicLength = topicData.length; + int totalPropLen = propertiesLen; + + // 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); + } + 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/MessageFilter.java b/store/src/main/java/org/apache/rocketmq/store/MessageFilter.java new file mode 100644 index 0000000..3dd0fee --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/MessageFilter.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 java.nio.ByteBuffer; +import java.util.Map; + +public interface MessageFilter { + /** + * match by tags code or filter bit map which is calculated when message received + * and stored in consume queue ext. + * + * @param tagsCode tagsCode + * @param cqExtUnit extend unit of consume queue + */ + boolean isMatchedByConsumeQueue(final Long tagsCode, + final ConsumeQueueExt.CqExtUnit cqExtUnit); + + /** + * match by message content which are stored in commit log. + *
    {@code msgBuffer} and {@code properties} are not all null.If invoked in store, + * {@code properties} is null;If invoked in {@code PullRequestHoldService}, {@code msgBuffer} is null. + * + * @param msgBuffer message buffer in commit log, may be null if not invoked in store. + * @param properties message properties, should decode from buffer if null by yourself. + */ + boolean isMatchedByCommitLog(final ByteBuffer msgBuffer, + final Map properties); +} diff --git a/store/src/main/java/org/apache/rocketmq/store/MessageStore.java b/store/src/main/java/org/apache/rocketmq/store/MessageStore.java new file mode 100644 index 0000000..4bbee14 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/MessageStore.java @@ -0,0 +1,994 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; +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 javax.annotation.Nonnull; +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.exception.ConsumeQueueException; +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. + */ +public interface MessageStore { + + /** + * Load previously stored messages. + * + * @return true if success; false otherwise. + */ + boolean load(); + + /** + * Launch this message store. + * + * @throws Exception if there is any error. + */ + void start() throws Exception; + + /** + * Shutdown this message store. + */ + void shutdown(); + + /** + * Destroy this message store. Generally, all persistent files should be removed after invocation. + */ + 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. + * + * @param msg Message instance to store + * @return result of store operation. + */ + PutMessageResult putMessage(final MessageExtBrokerInner msg); + + /** + * Store a batch of messages. + * + * @param messageExtBatch Message batch. + * @return result of storing batch messages. + */ + PutMessageResult putMessages(final MessageExtBatch messageExtBatch); + + /** + * 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 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 queueId Queue ID. + * @return Maximum offset at present. + */ + long getMaxOffsetInQueue(final String topic, final int queueId) throws ConsumeQueueException; + + /** + * 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) throws ConsumeQueueException; + + /** + * Get the minimum offset of the topic queue. + * + * @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 consumeQueueOffset offset of consume queue. + * @return physical offset. + */ + long getCommitLogOffsetInQueue(final String topic, final int queueId, final long consumeQueueOffset); + + /** + * 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 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. + * + * @param commitLogOffset physical offset. + * @return Message whose physical offset is as specified. + */ + 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. + * + * @param commitLogOffset commit log offset. + * @return wrapped result of the message. + */ + SelectMappedBufferResult selectOneMessageByOffset(final long commitLogOffset); + + /** + * Get one message from the specified commit log offset. + * + * @param commitLogOffset commit log offset. + * @param msgSize message size. + * @return wrapped result of the message. + */ + SelectMappedBufferResult selectOneMessageByOffset(final long commitLogOffset, final int msgSize); + + /** + * Get the running information of this store. + * + * @return message store running info. + */ + String getRunningDataInfo(); + + long getTimingMessageCount(String topic); + + /** + * Message store runtime information, which should generally contains various statistical information. + * + * @return runtime information of the message store in format of key-value pairs. + */ + HashMap getRuntimeInfo(); + + /** + * HA runtime information + * @return runtime information of ha + */ + HARuntimeInfo getHARuntimeInfo(); + + /** + * Get the maximum commit log offset. + * + * @return maximum commit log offset. + */ + long getMaxPhyOffset(); + + /** + * Get the minimum commit log offset. + * + * @return minimum commit log offset. + */ + long getMinPhyOffset(); + + /** + * Get the store time of the earliest message in the given queue. + * + * @param topic Topic of the messages to query. + * @param queueId Queue ID to find. + * @return store time of the earliest message. + */ + long getEarliestMessageTime(final String topic, final int queueId); + + /** + * Get the store time of the earliest message in this store. + * + * @return timestamp of the earliest message in this store. + */ + 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 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 queueId Queue ID. + * @return total number. + */ + long getMessageTotalInQueue(final String topic, final int queueId); + + /** + * Get the raw commit log data starting from the given offset, which should used for replication purpose. + * + * @param offset starting offset. + * @return commit log data. + */ + 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 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, int dataStart, int dataLength); + + /** + * Execute file deletion manually. + */ + void executeDeleteFilesManually(); + + /** + * Query messages by given 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. + */ + 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. + * + * @param newAddr new address. + */ + void updateHaMasterAddress(final String newAddr); + + /** + * Update master address. + * + * @param newAddr new address. + */ + void updateMasterAddress(final String newAddr); + + /** + * Return how much the slave falls behind. + * + * @return number of bytes that slave falls behind. + */ + long slaveFallBehindMuch(); + + /** + * Return the current timestamp of the store. + * + * @return current time in milliseconds since 1970-01-01. + */ + long now(); + + /** + * 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 retainTopics all valid topics. + * @return number of the topics deleted. + */ + int cleanUnusedTopic(final Set retainTopics); + + /** + * Clean expired consume queues. + */ + void cleanExpiredConsumerQueue(); + + /** + * Check if the given message has been swapped out of the memory. + * + * @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. + * + * @return number of the bytes to dispatch. + */ + long dispatchBehindBytes(); + + /** + * Get number of the milliseconds that have been stored in commit log and not yet dispatched to consume queue. + * + * @return number of the milliseconds to dispatch. + */ + long dispatchBehindMilliseconds(); + + /** + * Flush the message store to persist all data. + * + * @return maximum offset flushed to persistent storage device. + */ + long flush(); + + /** + * Get the current flushed offset. + * + * @return flushed offset + */ + long getFlushedWhere(); + + /** + * Reset written offset. + * + * @param phyOffset new offset. + * @return true if success; false otherwise. + */ + boolean resetWriteOffset(long phyOffset); + + /** + * Get confirm offset. + * + * @return confirm offset. + */ + long getConfirmOffset(); + + /** + * Set confirm offset. + * + * @param phyOffset confirm offset to set. + */ + void setConfirmOffset(long phyOffset); + + /** + * Check if the operating system page cache is busy or not. + * + * @return true if the OS page cache is busy; false otherwise. + */ + boolean isOSPageCacheBusy(); + + /** + * Get lock time in milliseconds of the store by far. + * + * @return lock time in milliseconds. + */ + long lockTimeMills(); + + /** + * Check if the transient store pool is deficient. + * + * @return true if the transient store pool is running out; false otherwise. + */ + boolean isTransientStorePoolDeficient(); + + /** + * Get the dispatcher list. + * + * @return list of the dispatcher. + */ + LinkedList getDispatcherList(); + + /** + * Add dispatcher. + * + * @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. + */ + 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; + + /** + * 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 + */ + @Nonnull + 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/MultiPathMappedFileQueue.java b/store/src/main/java/org/apache/rocketmq/store/MultiPathMappedFileQueue.java new file mode 100644 index 0000000..8ff050d --- /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 0000000..bf8832d --- /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/PutMessageLock.java b/store/src/main/java/org/apache/rocketmq/store/PutMessageLock.java new file mode 100644 index 0000000..758f437 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/PutMessageLock.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.store; + +/** + * Used when trying to put message + */ +public interface PutMessageLock { + void lock(); + + void unlock(); +} diff --git a/store/src/main/java/org/apache/rocketmq/store/PutMessageReentrantLock.java b/store/src/main/java/org/apache/rocketmq/store/PutMessageReentrantLock.java new file mode 100644 index 0000000..9aa80d8 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/PutMessageReentrantLock.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.locks.ReentrantLock; + +/** + * Exclusive lock implementation to put message + */ +public class PutMessageReentrantLock implements PutMessageLock { + private ReentrantLock putMessageNormalLock = new ReentrantLock(); // NonfairSync + + @Override + public void lock() { + putMessageNormalLock.lock(); + } + + @Override + public void unlock() { + putMessageNormalLock.unlock(); + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/PutMessageResult.java b/store/src/main/java/org/apache/rocketmq/store/PutMessageResult.java new file mode 100644 index 0000000..bcca6ae --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/PutMessageResult.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; + +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() { + 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() { + return appendMessageResult; + } + + public void setAppendMessageResult(AppendMessageResult appendMessageResult) { + this.appendMessageResult = appendMessageResult; + } + + public PutMessageStatus getPutMessageStatus() { + return putMessageStatus; + } + + 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 + ", remotePut=" + remotePut + "]"; + } + +} diff --git a/store/src/main/java/org/apache/rocketmq/store/PutMessageSpinLock.java b/store/src/main/java/org/apache/rocketmq/store/PutMessageSpinLock.java new file mode 100644 index 0000000..4243da0 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/PutMessageSpinLock.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; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Spin lock Implementation to put message, suggest using this with low race conditions + */ +public class PutMessageSpinLock implements PutMessageLock { + //true: Can lock, false : in lock. + private AtomicBoolean putMessageSpinLock = new AtomicBoolean(true); + + @Override + public void lock() { + boolean flag; + do { + flag = this.putMessageSpinLock.compareAndSet(true, false); + } + while (!flag); + } + + @Override + public void unlock() { + this.putMessageSpinLock.compareAndSet(false, true); + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/PutMessageStatus.java b/store/src/main/java/org/apache/rocketmq/store/PutMessageStatus.java new file mode 100644 index 0000000..55afd37 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/PutMessageStatus.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; + +public enum PutMessageStatus { + PUT_OK, + FLUSH_DISK_TIMEOUT, + FLUSH_SLAVE_TIMEOUT, + SLAVE_NOT_AVAILABLE, + SERVICE_NOT_AVAILABLE, + CREATE_MAPPED_FILE_FAILED, + MESSAGE_ILLEGAL, + PROPERTIES_SIZE_EXCEEDED, + 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 new file mode 100644 index 0000000..fbcbc05 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/QueryMessageResult.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.store; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +public class QueryMessageResult { + + private final List messageMapedList = + new ArrayList<>(100); + + private final List messageBufferList = new ArrayList<>(100); + private long indexLastUpdateTimestamp; + private long indexLastUpdatePhyoffset; + + private int bufferTotalSize = 0; + + public void addMessage(final SelectMappedBufferResult mapedBuffer) { + this.messageMapedList.add(mapedBuffer); + this.messageBufferList.add(mapedBuffer.getByteBuffer()); + this.bufferTotalSize += mapedBuffer.getSize(); + } + + public void release() { + for (SelectMappedBufferResult select : this.messageMapedList) { + select.release(); + } + } + + public long getIndexLastUpdateTimestamp() { + return indexLastUpdateTimestamp; + } + + public void setIndexLastUpdateTimestamp(long indexLastUpdateTimestamp) { + this.indexLastUpdateTimestamp = indexLastUpdateTimestamp; + } + + public long getIndexLastUpdatePhyoffset() { + return indexLastUpdatePhyoffset; + } + + public void setIndexLastUpdatePhyoffset(long indexLastUpdatePhyoffset) { + this.indexLastUpdatePhyoffset = indexLastUpdatePhyoffset; + } + + public List getMessageBufferList() { + return messageBufferList; + } + + public int getBufferTotalSize() { + return bufferTotalSize; + } + + public List getMessageMapedList() { + return messageMapedList; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/ReferenceResource.java b/store/src/main/java/org/apache/rocketmq/store/ReferenceResource.java new file mode 100644 index 0000000..dd4068a --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ReferenceResource.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; + +import java.util.concurrent.atomic.AtomicLong; + +public abstract class ReferenceResource { + protected final AtomicLong refCount = new AtomicLong(1); + protected volatile boolean available = true; + protected volatile boolean cleanupOver = false; + private volatile long firstShutdownTimestamp = 0; + + public synchronized boolean hold() { + if (this.isAvailable()) { + if (this.refCount.getAndIncrement() > 0) { + return true; + } else { + this.refCount.getAndDecrement(); + } + } + + return false; + } + + public boolean isAvailable() { + return this.available; + } + + public void shutdown(final long intervalForcibly) { + if (this.available) { + this.available = false; + this.firstShutdownTimestamp = System.currentTimeMillis(); + this.release(); + } else if (this.getRefCount() > 0) { + if ((System.currentTimeMillis() - this.firstShutdownTimestamp) >= intervalForcibly) { + this.refCount.set(-1000 - this.getRefCount()); + this.release(); + } + } + } + + public void release() { + long value = this.refCount.decrementAndGet(); + if (value > 0) + return; + + synchronized (this) { + + this.cleanupOver = this.cleanup(value); + } + } + + public long getRefCount() { + return this.refCount.get(); + } + + public abstract boolean cleanup(final long currentRef); + + public boolean isCleanupOver() { + return this.refCount.get() <= 0 && this.cleanupOver; + } +} 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 0000000..321689a --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/RocksDBMessageStore.java @@ -0,0 +1,208 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.metrics.Meter; +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Supplier; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +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 { + + private CommitLogDispatcherBuildRocksdbConsumeQueue dispatcherBuildRocksdbConsumeQueue; + + 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 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 void initMetrics(Meter meter, Supplier attributesBuilderSupplier) { + DefaultStoreMetricsManager.init(meter, attributesBuilderSupplier, this); + // Also add some metrics for rocksdb's monitoring. + RocksDBStoreMetricsManager.init(meter, attributesBuilderSupplier, this); + } + + public CommitLogDispatcherBuildRocksdbConsumeQueue getDispatcherBuildRocksdbConsumeQueue() { + return dispatcherBuildRocksdbConsumeQueue; + } + + class CommitLogDispatcherBuildRocksdbConsumeQueue implements CommitLogDispatcher { + @Override + public void dispatch(DispatchRequest request) throws RocksDBException { + boolean enable = getMessageStoreConfig().isRocksdbCQDoubleWriteEnable(); + if (!enable) { + return; + } + final int tranType = MessageSysFlag.getTransactionValue(request.getSysFlag()); + switch (tranType) { + case MessageSysFlag.TRANSACTION_NOT_TYPE: + case MessageSysFlag.TRANSACTION_COMMIT_TYPE: + putMessagePositionInfo(request); + break; + case MessageSysFlag.TRANSACTION_PREPARED_TYPE: + case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE: + break; + } + } + } + + public void loadAndStartConsumerServiceOnly() { + try { + this.dispatcherBuildRocksdbConsumeQueue = new CommitLogDispatcherBuildRocksdbConsumeQueue(); + boolean loadResult = this.consumeQueueStore.load(); + if (!loadResult) { + throw new RuntimeException("load consume queue failed"); + } + super.loadCheckPoint(); + this.consumeQueueStore.start(); + } catch (Exception e) { + ERROR_LOG.error("loadAndStartConsumerServiceOnly error", e); + throw new RuntimeException(e); + } + } + +} diff --git a/store/src/main/java/org/apache/rocketmq/store/RunningFlags.java b/store/src/main/java/org/apache/rocketmq/store/RunningFlags.java new file mode 100644 index 0000000..88b398a --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/RunningFlags.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.store; + +public class RunningFlags { + + private static final int NOT_READABLE_BIT = 1; + + private static final int NOT_WRITEABLE_BIT = 1 << 1; + + private static final int WRITE_LOGICS_QUEUE_ERROR_BIT = 1 << 2; + + private static final int WRITE_INDEX_FILE_ERROR_BIT = 1 << 3; + + 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() { + } + + public int getFlagBits() { + return flagBits; + } + + public boolean getAndMakeReadable() { + boolean result = this.isReadable(); + if (!result) { + this.flagBits &= ~NOT_READABLE_BIT; + } + return result; + } + + public boolean isReadable() { + return (this.flagBits & NOT_READABLE_BIT) == 0; + } + + public boolean isFenced() { + return (this.flagBits & FENCED_BIT) != 0; + } + + public boolean getAndMakeNotReadable() { + boolean result = this.isReadable(); + if (result) { + this.flagBits |= NOT_READABLE_BIT; + } + return result; + } + + public void clearLogicsQueueError() { + this.flagBits &= ~WRITE_LOGICS_QUEUE_ERROR_BIT; + } + + public boolean getAndMakeWriteable() { + boolean result = this.isWriteable(); + if (!result) { + this.flagBits &= ~NOT_WRITEABLE_BIT; + } + return result; + } + + public boolean isWriteable() { + 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; + } + + return false; + } + + //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 | LOGIC_DISK_FULL_BIT)) == 0) { + return true; + } + + return false; + } + + public boolean getAndMakeNotWriteable() { + boolean result = this.isWriteable(); + if (result) { + this.flagBits |= NOT_WRITEABLE_BIT; + } + return result; + } + + 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; + } + + return false; + } + + public void makeIndexFileError() { + this.flagBits |= WRITE_INDEX_FILE_ERROR_BIT; + } + + public boolean isIndexFileError() { + if ((this.flagBits & WRITE_INDEX_FILE_ERROR_BIT) == WRITE_INDEX_FILE_ERROR_BIT) { + return true; + } + + return false; + } + + public boolean getAndMakeDiskFull() { + boolean result = !((this.flagBits & DISK_FULL_BIT) == DISK_FULL_BIT); + this.flagBits |= DISK_FULL_BIT; + return result; + } + + public boolean getAndMakeDiskOK() { + boolean result = !((this.flagBits & DISK_FULL_BIT) == DISK_FULL_BIT); + 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 new file mode 100644 index 0000000..5c38cfe --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/SelectMappedBufferResult.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.store; + +import java.nio.ByteBuffer; +import org.apache.rocketmq.store.logfile.MappedFile; + +public class SelectMappedBufferResult { + + private final long startOffset; + + private final ByteBuffer byteBuffer; + + private int size; + + protected MappedFile mappedFile; + + private boolean isInCache = true; + + public SelectMappedBufferResult(long startOffset, ByteBuffer byteBuffer, int size, MappedFile mappedFile) { + this.startOffset = startOffset; + this.byteBuffer = byteBuffer; + this.size = size; + this.mappedFile = mappedFile; + } + + public ByteBuffer getByteBuffer() { + return byteBuffer; + } + + public int getSize() { + return size; + } + + public void setSize(final int s) { + this.size = s; + this.byteBuffer.limit(this.size); + } + + public MappedFile getMappedFile() { + return mappedFile; + } + + 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 0000000..9655f28 --- /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 new file mode 100644 index 0000000..1e2504a --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/StoreCheckpoint.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.store; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileChannel.MapMode; +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.logfile.DefaultMappedFile; + +public class StoreCheckpoint { + 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 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); + 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("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)); + log.info("store checkpoint file logicsMsgTimestamp " + this.logicsMsgTimestamp + ", " + + 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); + } + } + + public void shutdown() { + this.flush(); + + // unmap mappedByteBuffer + UtilAll.cleanBuffer(this.mappedByteBuffer); + + try { + this.fileChannel.close(); + } catch (IOException e) { + log.error("Failed to properly close the channel", e); + } + } + + 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(); + } + + public long getPhysicMsgTimestamp() { + return physicMsgTimestamp; + } + + public void setPhysicMsgTimestamp(long physicMsgTimestamp) { + this.physicMsgTimestamp = physicMsgTimestamp; + } + + public long getLogicsMsgTimestamp() { + return logicsMsgTimestamp; + } + + 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); + } + + public long getMinTimestamp() { + long min = Math.min(this.physicMsgTimestamp, this.logicsMsgTimestamp); + + min -= 1000 * 3; + if (min < 0) { + min = 0; + } + + return min; + } + + public long getIndexMsgTimestamp() { + return indexMsgTimestamp; + } + + 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 new file mode 100644 index 0000000..1969b14 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/StoreStatsService.java @@ -0,0 +1,681 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.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); + + private static final int FREQUENCY_OF_SAMPLING = 1000; + + private static final int MAX_RECORDS_OF_SAMPLING = 60 * 10; + private static final String[] PUT_MESSAGE_ENTIRE_TIME_MAX_DESC = new String[] { + "[<=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 LongAdder putMessageFailedTimes = new LongAdder(); + + private final ConcurrentMap putMessageTopicTimesTotal = + new ConcurrentHashMap<>(128); + private final ConcurrentMap putMessageTopicSizeTotal = + new ConcurrentHashMap<>(128); + + 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 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 putLock = new ReentrantLock(); + // for getMessageEntireTimeMax + private ReentrantLock getLock = new ReentrantLock(); + + private volatile long dispatchMaxBuffer = 0; + + private ReentrantLock samplingLock = new ReentrantLock(); + private long lastPrintTimestamp = System.currentTimeMillis(); + + private BrokerIdentity brokerIdentity; + + public StoreStatsService(BrokerIdentity brokerIdentity) { + this(); + this.brokerIdentity = brokerIdentity; + } + + public StoreStatsService() { + 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; + } + + 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 LongAdder(); + } + + this.lastPutMessageDistributeTime = this.putMessageDistributeTime; + + this.putMessageDistributeTime = next; + + return lastPutMessageDistributeTime; + } + + public long getPutMessageEntireTimeMax() { + return putMessageEntireTimeMax; + } + + public void setPutMessageEntireTimeMax(long value) { + this.incPutMessageEntireTime(value); + final LongAdder[] times = this.putMessageDistributeTime; + + if (null == times) + return; + + // us + if (value <= 0) { + times[0].add(1); + } else if (value < 10) { + times[1].add(1); + } else if (value < 50) { + times[2].add(1); + } else if (value < 100) { + times[3].add(1); + } else if (value < 200) { + times[4].add(1); + } else if (value < 500) { + times[5].add(1); + } else if (value < 1000) { + times[6].add(1); + } + // 2s + else if (value < 2000) { + times[7].add(1); + } + // 3s + else if (value < 3000) { + times[8].add(1); + } + // 4s + else if (value < 4000) { + times[9].add(1); + } + // 5s + else if (value < 5000) { + times[10].add(1); + } + // 10s + else if (value < 10000) { + times[11].add(1); + } else { + times[12].add(1); + } + + if (value > this.putMessageEntireTimeMax) { + this.putLock.lock(); + this.putMessageEntireTimeMax = + value > this.putMessageEntireTimeMax ? value : this.putMessageEntireTimeMax; + this.putLock.unlock(); + } + } + + public long getGetMessageEntireTimeMax() { + return getMessageEntireTimeMax; + } + + public void setGetMessageEntireTimeMax(long value) { + if (value > this.getMessageEntireTimeMax) { + this.getLock.lock(); + this.getMessageEntireTimeMax = + value > this.getMessageEntireTimeMax ? value : this.getMessageEntireTimeMax; + this.getLock.unlock(); + } + } + + public long getDispatchMaxBuffer() { + return dispatchMaxBuffer; + } + + public void setDispatchMaxBuffer(long value) { + this.dispatchMaxBuffer = value > this.dispatchMaxBuffer ? value : this.dispatchMaxBuffer; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(1024); + Long totalTimes = getPutMessageTimesTotal(); + if (0 == totalTimes) { + totalTimes = 1L; + } + + 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"); + sb.append("\tputMessageAverageSize: " + (this.getPutMessageSizeTotal() / totalTimes.doubleValue()) + + "\r\n"); + sb.append("\tdispatchMaxBuffer: " + this.dispatchMaxBuffer + "\r\n"); + sb.append("\tgetMessageEntireTimeMax: " + this.getMessageEntireTimeMax + "\r\n"); + sb.append("\tputTps: " + this.getPutTps() + "\r\n"); + sb.append("\tgetFoundTps: " + this.getGetFoundTps() + "\r\n"); + sb.append("\tgetMissTps: " + this.getGetMissTps() + "\r\n"); + sb.append("\tgetTotalTps: " + this.getGetTotalTps() + "\r\n"); + sb.append("\tgetTransferredTps: " + this.getGetTransferredTps() + "\r\n"); + return sb.toString(); + } + + public long getPutMessageTimesTotal() { + Map map = putMessageTopicTimesTotal; + return map.values() + .parallelStream() + .mapToLong(LongAdder::longValue) + .sum(); + } + + private String getFormatRuntime() { + final long millisecond = 1; + final long second = 1000 * millisecond; + final long minute = 60 * second; + final long hour = 60 * minute; + final long day = 24 * hour; + final MessageFormat messageFormat = new MessageFormat("[ {0} days, {1} hours, {2} minutes, {3} seconds ]"); + + long time = System.currentTimeMillis() - this.messageStoreBootTimestamp; + long days = time / day; + long hours = (time % day) / hour; + long minutes = (time % hour) / minute; + long seconds = (time % minute) / second; + return messageFormat.format(new Long[] {days, hours, minutes, seconds}); + } + + public long getPutMessageSizeTotal() { + Map map = putMessageTopicSizeTotal; + return map.values() + .parallelStream() + .mapToLong(LongAdder::longValue) + .sum(); + } + + private String getPutMessageDistributeTimeStringInfo(Long total) { + return this.putMessageDistributeTimeToString(); + } + + private String getPutTps() { + StringBuilder sb = new StringBuilder(); + + sb.append(this.getPutTps(10)); + sb.append(" "); + + sb.append(this.getPutTps(60)); + sb.append(" "); + + sb.append(this.getPutTps(600)); + + return sb.toString(); + } + + private String getGetFoundTps() { + StringBuilder sb = new StringBuilder(); + + sb.append(this.getGetFoundTps(10)); + sb.append(" "); + + sb.append(this.getGetFoundTps(60)); + sb.append(" "); + + sb.append(this.getGetFoundTps(600)); + + return sb.toString(); + } + + private String getGetMissTps() { + StringBuilder sb = new StringBuilder(); + + sb.append(this.getGetMissTps(10)); + sb.append(" "); + + sb.append(this.getGetMissTps(60)); + sb.append(" "); + + sb.append(this.getGetMissTps(600)); + + return sb.toString(); + } + + private String getGetTotalTps() { + StringBuilder sb = new StringBuilder(); + + sb.append(this.getGetTotalTps(10)); + sb.append(" "); + + sb.append(this.getGetTotalTps(60)); + sb.append(" "); + + sb.append(this.getGetTotalTps(600)); + + return sb.toString(); + } + + private String getGetTransferredTps() { + StringBuilder sb = new StringBuilder(); + + sb.append(this.getGetTransferredTps(10)); + sb.append(" "); + + sb.append(this.getGetTransferredTps(60)); + sb.append(" "); + + sb.append(this.getGetTransferredTps(600)); + + return sb.toString(); + } + + private String putMessageDistributeTimeToString() { + 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].longValue(); + sb.append(String.format("%s:%d", PUT_MESSAGE_ENTIRE_TIME_MAX_DESC[i], value)); + sb.append(" "); + } + + return sb.toString(); + } + + private String getPutTps(int time) { + String result = ""; + this.samplingLock.lock(); + try { + CallSnapshot last = this.putTimesList.getLast(); + + if (this.putTimesList.size() > time) { + CallSnapshot lastBefore = this.putTimesList.get(this.putTimesList.size() - (time + 1)); + result += CallSnapshot.getTPS(lastBefore, last); + } + + } finally { + this.samplingLock.unlock(); + } + return result; + } + + private String getGetFoundTps(int time) { + String result = ""; + this.samplingLock.lock(); + try { + CallSnapshot last = this.getTimesFoundList.getLast(); + + if (this.getTimesFoundList.size() > time) { + CallSnapshot lastBefore = + this.getTimesFoundList.get(this.getTimesFoundList.size() - (time + 1)); + result += CallSnapshot.getTPS(lastBefore, last); + } + } finally { + this.samplingLock.unlock(); + } + + return result; + } + + private String getGetMissTps(int time) { + String result = ""; + this.samplingLock.lock(); + try { + CallSnapshot last = this.getTimesMissList.getLast(); + + if (this.getTimesMissList.size() > time) { + CallSnapshot lastBefore = + this.getTimesMissList.get(this.getTimesMissList.size() - (time + 1)); + result += CallSnapshot.getTPS(lastBefore, last); + } + + } finally { + this.samplingLock.unlock(); + } + + return result; + } + + private String getGetTotalTps(int time) { + this.samplingLock.lock(); + double found = 0; + double miss = 0; + try { + { + CallSnapshot last = this.getTimesFoundList.getLast(); + + if (this.getTimesFoundList.size() > time) { + CallSnapshot lastBefore = + this.getTimesFoundList.get(this.getTimesFoundList.size() - (time + 1)); + found = CallSnapshot.getTPS(lastBefore, last); + } + } + { + CallSnapshot last = this.getTimesMissList.getLast(); + + if (this.getTimesMissList.size() > time) { + CallSnapshot lastBefore = + this.getTimesMissList.get(this.getTimesMissList.size() - (time + 1)); + miss = CallSnapshot.getTPS(lastBefore, last); + } + } + + } finally { + this.samplingLock.unlock(); + } + + return Double.toString(found + miss); + } + + private String getGetTransferredTps(int time) { + String result = ""; + this.samplingLock.lock(); + try { + CallSnapshot last = this.transferredMsgCountList.getLast(); + + if (this.transferredMsgCountList.size() > time) { + CallSnapshot lastBefore = + this.transferredMsgCountList.get(this.transferredMsgCountList.size() - (time + 1)); + result += CallSnapshot.getTPS(lastBefore, last); + } + + } finally { + this.samplingLock.unlock(); + } + + return result; + } + + public HashMap getRuntimeInfo() { + HashMap result = new HashMap<>(64); + + Long totalTimes = getPutMessageTimesTotal(); + if (0 == totalTimes) { + totalTimes = 1L; + } + + result.put("bootTimestamp", String.valueOf(this.messageStoreBootTimestamp)); + 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))); + result.put("putMessageAverageSize", + String.valueOf(this.getPutMessageSizeTotal() / totalTimes.doubleValue())); + result.put("dispatchMaxBuffer", String.valueOf(this.dispatchMaxBuffer)); + result.put("getMessageEntireTimeMax", String.valueOf(this.getMessageEntireTimeMax)); + 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; + } + + public void run() { + log.info(this.getServiceName() + " service started"); + + while (!this.isStopped()) { + try { + this.waitForRunning(FREQUENCY_OF_SAMPLING); + + this.sampling(); + + this.printTps(); + } catch (Exception e) { + log.warn(this.getServiceName() + " service has exception. ", e); + } + } + + log.info(this.getServiceName() + " service end"); + } + + @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.samplingLock.lock(); + try { + this.putTimesList.add(new CallSnapshot(System.currentTimeMillis(), getPutMessageTimesTotal())); + if (this.putTimesList.size() > (MAX_RECORDS_OF_SAMPLING + 1)) { + this.putTimesList.removeFirst(); + } + + this.getTimesFoundList.add(new CallSnapshot(System.currentTimeMillis(), + this.getMessageTimesTotalFound.longValue())); + if (this.getTimesFoundList.size() > (MAX_RECORDS_OF_SAMPLING + 1)) { + this.getTimesFoundList.removeFirst(); + } + + this.getTimesMissList.add(new CallSnapshot(System.currentTimeMillis(), + this.getMessageTimesTotalMiss.longValue())); + if (this.getTimesMissList.size() > (MAX_RECORDS_OF_SAMPLING + 1)) { + this.getTimesMissList.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.samplingLock.unlock(); + } + } + + 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_transferred_tps {}", + this.getPutTps(printTPSInterval), + this.getGetFoundTps(printTPSInterval), + this.getGetMissTps(printTPSInterval), + this.getGetTransferredTps(printTPSInterval) + ); + + 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].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 LongAdder getGetMessageTimesTotalFound() { + return getMessageTimesTotalFound; + } + + public LongAdder getGetMessageTimesTotalMiss() { + return getMessageTimesTotalMiss; + } + + public LongAdder getGetMessageTransferredMsgCount() { + return getMessageTransferredMsgCount; + } + + public LongAdder getPutMessageFailedTimes() { + return putMessageFailedTimes; + } + + public LongAdder getSinglePutMessageTopicSizeTotal(String topic) { + LongAdder rs = putMessageTopicSizeTotal.get(topic); + if (null == rs) { + rs = new LongAdder(); + LongAdder previous = putMessageTopicSizeTotal.putIfAbsent(topic, rs); + if (previous != null) { + rs = previous; + } + } + return rs; + } + + public LongAdder getSinglePutMessageTopicTimesTotal(String topic) { + LongAdder rs = putMessageTopicTimesTotal.get(topic); + if (null == rs) { + rs = new LongAdder(); + LongAdder previous = putMessageTopicTimesTotal.putIfAbsent(topic, rs); + if (previous != null) { + rs = previous; + } + } + return rs; + } + + public Map getPutMessageTopicTimesTotal() { + return putMessageTopicTimesTotal; + } + + public Map getPutMessageTopicSizeTotal() { + return putMessageTopicSizeTotal; + } + + static class CallSnapshot { + public final long timestamp; + public final long callTimesTotal; + + public CallSnapshot(long timestamp, long callTimesTotal) { + this.timestamp = timestamp; + this.callTimesTotal = callTimesTotal; + } + + public static double getTPS(final CallSnapshot begin, final CallSnapshot end) { + long total = end.callTimesTotal - begin.callTimesTotal; + Long time = end.timestamp - begin.timestamp; + + double tps = total / time.doubleValue(); + + return tps * 1000; + } + } +} 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 0000000..4f9c4d0 --- /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 new file mode 100644 index 0000000..526ca9b --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/StoreUtil.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.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") + public static long getTotalPhysicalMemorySize() { + long physicalTotal = 1024 * 1024 * 1024 * 24L; + OperatingSystemMXBean osmxb = ManagementFactory.getOperatingSystemMXBean(); + if (osmxb instanceof com.sun.management.OperatingSystemMXBean) { + physicalTotal = ((com.sun.management.OperatingSystemMXBean) osmxb).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 0000000..cb8dee5 --- /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 0000000..5a131b5 --- /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 new file mode 100644 index 0000000..0d42ee6 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/TransientStorePool.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; + +import com.sun.jna.NativeLong; +import com.sun.jna.Pointer; +import java.nio.ByteBuffer; +import java.util.Deque; +import java.util.concurrent.ConcurrentLinkedDeque; +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.util.LibC; +import sun.nio.ch.DirectBuffer; + +public class TransientStorePool { + private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + + private final int poolSize; + private final int fileSize; + private final Deque availableBuffers; + private volatile boolean isRealCommit = true; + + public TransientStorePool(final int poolSize, final int fileSize) { + this.poolSize = poolSize; + this.fileSize = fileSize; + this.availableBuffers = new ConcurrentLinkedDeque<>(); + } + + /** + * It's a heavy init method. + */ + public void init() { + for (int i = 0; i < poolSize; i++) { + ByteBuffer byteBuffer = ByteBuffer.allocateDirect(fileSize); + + final long address = ((DirectBuffer) byteBuffer).address(); + Pointer pointer = new Pointer(address); + LibC.INSTANCE.mlock(pointer, new NativeLong(fileSize)); + + availableBuffers.offer(byteBuffer); + } + } + + public void destroy() { + for (ByteBuffer byteBuffer : availableBuffers) { + final long address = ((DirectBuffer) byteBuffer).address(); + Pointer pointer = new Pointer(address); + LibC.INSTANCE.munlock(pointer, new NativeLong(fileSize)); + } + } + + public void returnBuffer(ByteBuffer byteBuffer) { + byteBuffer.position(0); + byteBuffer.limit(fileSize); + this.availableBuffers.offerFirst(byteBuffer); + } + + public ByteBuffer borrowBuffer() { + ByteBuffer buffer = availableBuffers.pollFirst(); + if (availableBuffers.size() < poolSize * 0.4) { + log.warn("TransientStorePool only remain {} sheets.", availableBuffers.size()); + } + return buffer; + } + + 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/BrokerRole.java b/store/src/main/java/org/apache/rocketmq/store/config/BrokerRole.java new file mode 100644 index 0000000..4f02d6c --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/config/BrokerRole.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.config; + +public enum BrokerRole { + ASYNC_MASTER, + SYNC_MASTER, + SLAVE; +} diff --git a/store/src/main/java/org/apache/rocketmq/store/config/FlushDiskType.java b/store/src/main/java/org/apache/rocketmq/store/config/FlushDiskType.java new file mode 100644 index 0000000..2288530 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/config/FlushDiskType.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.config; + +public enum FlushDiskType { + SYNC_FLUSH, + ASYNC_FLUSH +} 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 new file mode 100644 index 0000000..0ea5841 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/config/MessageStoreConfig.java @@ -0,0 +1,1953 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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; +import org.rocksdb.CompressionType; +import org.rocksdb.util.SizeUnit; + +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 = 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 mappedFileSizeCommitLog = 1024 * 1024 * 1024; + + // CompactionLog 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 boolean timerEnableRetryUntilSuccess = 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 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; + + // CommitLog flush interval + // flush data to disk + @ImportantField + private int flushIntervalCommitLog = 500; + + // Only used if TransientStorePool enabled + // flush data to FileChannel + @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.
    + */ + private boolean useReentrantLockWhenPutMessage = true; + + // Whether schedule flush + @ImportantField + private boolean flushCommitLogTimed = true; + // ConsumeQueue flush interval + private int flushIntervalConsumeQueue = 1000; + // Resource reclaim interval + private int cleanResourceInterval = 10000; + // CommitLog removal interval + private int deleteCommitLogFilesInterval = 100; + // ConsumeQueue removal interval + private int deleteConsumeQueueFilesInterval = 100; + private int destroyMapedFileIntervalForcibly = 1000 * 120; + private int redeleteHangedFileInterval = 1000 * 120; + // When to delete,default is at 4 am + @ImportantField + private String deleteWhen = "04"; + private int diskMaxUsedSpaceRatio = 75; + // 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 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. + private boolean checkCRCOnRecover = true; + // How many pages are to be flushed when flush CommitLog + private int flushCommitLogLeastPages = 4; + // How many pages are to be committed when commit data to file + private int commitCommitLogLeastPages = 4; + // Flush page size when the disk in warming state + private int flushLeastPagesWhenWarmMapedFile = 1024 / 4 * 16; + // How many pages are to be flushed when flush ConsumeQueue + private int flushConsumeQueueLeastPages = 2; + private int flushCommitLogThoroughInterval = 1000 * 10; + private int commitCommitLogThoroughInterval = 200; + private int flushConsumeQueueThoroughInterval = 1000 * 60; + @ImportantField + private int maxTransferBytesOnMessageInMemory = 1024 * 256; + @ImportantField + private int maxTransferCountOnMessageInMemory = 32; + @ImportantField + private int maxTransferBytesOnMessageInDisk = 1024 * 64; + @ImportantField + private int maxTransferCountOnMessageInDisk = 8; + @ImportantField + private int accessMessageInMemoryMaxRatio = 40; + @ImportantField + private boolean messageIndexEnable = true; + private int maxHashSlotNum = 5000000; + private int maxIndexNum = 5000000 * 4; + private int maxMsgsNumBatch = 64; + @ImportantField + private boolean messageIndexSafe = false; + 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 haMaxGapNotInSync = 1024 * 1024 * 256; + @ImportantField + 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 + private boolean cleanFileForciblyEnable = true; + private boolean warmMapedFileEnable = false; + private boolean offsetCheckInSlave = false; + private boolean debugLockEnable = false; + private boolean duplicationEnable = false; + private boolean diskFallRecorded = true; + private long osPageCacheBusyTimeOutMills = 1000; + private int defaultQueryMaxNum = 32; + + @ImportantField + private boolean transientStorePoolEnable = false; + 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; + + /** + * If readUnCommitted is true, the dispatch of the consume queue will exceed the confirmOffset, which may cause the client to read uncommitted messages. + * For example, reput offset exceeding the flush offset during synchronous disk flushing. + */ + private boolean readUnCommitted = false; + + private boolean putConsumeQueueDataByFileChannel = true; + + private boolean rocksdbCQDoubleWriteEnable = false; + + /** + * If ConsumeQueueStore is RocksDB based, this option is to configure bottom-most tier compression type. + * The following values are valid: + *
      + *
    • snappy
    • + *
    • z
    • + *
    • bzip2
    • + *
    • lz4
    • + *
    • lz4hc
    • + *
    • xpress
    • + *
    • zstd
    • + *
    + * + * LZ4 is the recommended one. + */ + private String bottomMostCompressionTypeForConsumeQueueStore = CompressionType.ZSTD_COMPRESSION.getLibraryName(); + + private String rocksdbCompressionType = CompressionType.LZ4_COMPRESSION.getLibraryName(); + + /** + * Flush RocksDB WAL frequency, aka, flush WAL every N write ops. + */ + private int rocksdbFlushWalFrequency = 1024; + + private long rocksdbWalFileRollingThreshold = SizeUnit.GB; + + public String getRocksdbCompressionType() { + return rocksdbCompressionType; + } + + public void setRocksdbCompressionType(String compressionType) { + this.rocksdbCompressionType = compressionType; + } + + /** + * Spin number in the retreat strategy of spin lock + * Default is 1000 + */ + private int spinLockCollisionRetreatOptimalDegree = 1000; + + /** + * Use AdaptiveBackOffLock + **/ + private boolean useABSLock = false; + + public boolean isRocksdbCQDoubleWriteEnable() { + return rocksdbCQDoubleWriteEnable; + } + + public void setRocksdbCQDoubleWriteEnable(boolean rocksdbWriteEnable) { + this.rocksdbCQDoubleWriteEnable = rocksdbWriteEnable; + } + + + public boolean isEnabledAppendPropCRC() { + return enabledAppendPropCRC; + } + + public void setEnabledAppendPropCRC(boolean enabledAppendPropCRC) { + this.enabledAppendPropCRC = enabledAppendPropCRC; + } + + public boolean isDebugLockEnable() { + return debugLockEnable; + } + + public void setDebugLockEnable(final boolean debugLockEnable) { + this.debugLockEnable = debugLockEnable; + } + + public boolean isDuplicationEnable() { + return duplicationEnable; + } + + public void setDuplicationEnable(final boolean duplicationEnable) { + this.duplicationEnable = duplicationEnable; + } + + public long getOsPageCacheBusyTimeOutMills() { + return osPageCacheBusyTimeOutMills; + } + + public void setOsPageCacheBusyTimeOutMills(final long osPageCacheBusyTimeOutMills) { + this.osPageCacheBusyTimeOutMills = osPageCacheBusyTimeOutMills; + } + + public boolean isDiskFallRecorded() { + return diskFallRecorded; + } + + public void setDiskFallRecorded(final boolean diskFallRecorded) { + this.diskFallRecorded = diskFallRecorded; + } + + public boolean isWarmMapedFileEnable() { + return warmMapedFileEnable; + } + + public void setWarmMapedFileEnable(boolean warmMapedFileEnable) { + this.warmMapedFileEnable = warmMapedFileEnable; + } + + 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 String getStoreType() { + return storeType; + } + + public void setStoreType(String storeType) { + this.storeType = storeType; + } + + 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 setMappedFileSizeConsumeQueue(int mappedFileSizeConsumeQueue) { + this.mappedFileSizeConsumeQueue = mappedFileSizeConsumeQueue; + } + + public boolean isEnableConsumeQueueExt() { + return enableConsumeQueueExt; + } + + public void setEnableConsumeQueueExt(boolean enableConsumeQueueExt) { + this.enableConsumeQueueExt = enableConsumeQueueExt; + } + + public int getMappedFileSizeConsumeQueueExt() { + return mappedFileSizeConsumeQueueExt; + } + + public void setMappedFileSizeConsumeQueueExt(int mappedFileSizeConsumeQueueExt) { + this.mappedFileSizeConsumeQueueExt = mappedFileSizeConsumeQueueExt; + } + + public int getBitMapLengthConsumeQueueExt() { + return bitMapLengthConsumeQueueExt; + } + + public void setBitMapLengthConsumeQueueExt(int bitMapLengthConsumeQueueExt) { + this.bitMapLengthConsumeQueueExt = bitMapLengthConsumeQueueExt; + } + + public int getFlushIntervalCommitLog() { + return flushIntervalCommitLog; + } + + public void setFlushIntervalCommitLog(int flushIntervalCommitLog) { + this.flushIntervalCommitLog = flushIntervalCommitLog; + } + + public int getFlushIntervalConsumeQueue() { + return flushIntervalConsumeQueue; + } + + public void setFlushIntervalConsumeQueue(int flushIntervalConsumeQueue) { + this.flushIntervalConsumeQueue = flushIntervalConsumeQueue; + } + + public int getPutMsgIndexHightWater() { + return putMsgIndexHightWater; + } + + public void setPutMsgIndexHightWater(int putMsgIndexHightWater) { + this.putMsgIndexHightWater = putMsgIndexHightWater; + } + + public int getCleanResourceInterval() { + return cleanResourceInterval; + } + + public void setCleanResourceInterval(int cleanResourceInterval) { + this.cleanResourceInterval = cleanResourceInterval; + } + + public int getMaxMessageSize() { + return maxMessageSize; + } + + 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; + } + + public boolean getCheckCRCOnRecover() { + return checkCRCOnRecover; + } + + 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; + } + + 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; + } + + public void setDeleteWhen(String deleteWhen) { + this.deleteWhen = deleteWhen; + } + + public int getDiskMaxUsedSpaceRatio() { + if (this.diskMaxUsedSpaceRatio < 10) + return 10; + + if (this.diskMaxUsedSpaceRatio > 95) + return 95; + + return diskMaxUsedSpaceRatio; + } + + public void setDiskMaxUsedSpaceRatio(int diskMaxUsedSpaceRatio) { + this.diskMaxUsedSpaceRatio = diskMaxUsedSpaceRatio; + } + + public int getDeleteCommitLogFilesInterval() { + return deleteCommitLogFilesInterval; + } + + public void setDeleteCommitLogFilesInterval(int deleteCommitLogFilesInterval) { + this.deleteCommitLogFilesInterval = deleteCommitLogFilesInterval; + } + + public int getDeleteConsumeQueueFilesInterval() { + return deleteConsumeQueueFilesInterval; + } + + public void setDeleteConsumeQueueFilesInterval(int deleteConsumeQueueFilesInterval) { + this.deleteConsumeQueueFilesInterval = deleteConsumeQueueFilesInterval; + } + + public int getMaxTransferBytesOnMessageInMemory() { + return maxTransferBytesOnMessageInMemory; + } + + public void setMaxTransferBytesOnMessageInMemory(int maxTransferBytesOnMessageInMemory) { + this.maxTransferBytesOnMessageInMemory = maxTransferBytesOnMessageInMemory; + } + + public int getMaxTransferCountOnMessageInMemory() { + return maxTransferCountOnMessageInMemory; + } + + public void setMaxTransferCountOnMessageInMemory(int maxTransferCountOnMessageInMemory) { + this.maxTransferCountOnMessageInMemory = maxTransferCountOnMessageInMemory; + } + + public int getMaxTransferBytesOnMessageInDisk() { + return maxTransferBytesOnMessageInDisk; + } + + public void setMaxTransferBytesOnMessageInDisk(int maxTransferBytesOnMessageInDisk) { + this.maxTransferBytesOnMessageInDisk = maxTransferBytesOnMessageInDisk; + } + + public int getMaxTransferCountOnMessageInDisk() { + return maxTransferCountOnMessageInDisk; + } + + public void setMaxTransferCountOnMessageInDisk(int maxTransferCountOnMessageInDisk) { + this.maxTransferCountOnMessageInDisk = maxTransferCountOnMessageInDisk; + } + + public int getFlushCommitLogLeastPages() { + return flushCommitLogLeastPages; + } + + public void setFlushCommitLogLeastPages(int flushCommitLogLeastPages) { + this.flushCommitLogLeastPages = flushCommitLogLeastPages; + } + + public int getFlushConsumeQueueLeastPages() { + return flushConsumeQueueLeastPages; + } + + public void setFlushConsumeQueueLeastPages(int flushConsumeQueueLeastPages) { + this.flushConsumeQueueLeastPages = flushConsumeQueueLeastPages; + } + + public int getFlushCommitLogThoroughInterval() { + return flushCommitLogThoroughInterval; + } + + public void setFlushCommitLogThoroughInterval(int flushCommitLogThoroughInterval) { + this.flushCommitLogThoroughInterval = flushCommitLogThoroughInterval; + } + + public int getFlushConsumeQueueThoroughInterval() { + return flushConsumeQueueThoroughInterval; + } + + public void setFlushConsumeQueueThoroughInterval(int flushConsumeQueueThoroughInterval) { + this.flushConsumeQueueThoroughInterval = flushConsumeQueueThoroughInterval; + } + + public int getDestroyMapedFileIntervalForcibly() { + return destroyMapedFileIntervalForcibly; + } + + public void setDestroyMapedFileIntervalForcibly(int destroyMapedFileIntervalForcibly) { + this.destroyMapedFileIntervalForcibly = destroyMapedFileIntervalForcibly; + } + + public int getFileReservedTime() { + return fileReservedTime; + } + + public void setFileReservedTime(int fileReservedTime) { + this.fileReservedTime = fileReservedTime; + } + + public int getRedeleteHangedFileInterval() { + return redeleteHangedFileInterval; + } + + public void setRedeleteHangedFileInterval(int redeleteHangedFileInterval) { + this.redeleteHangedFileInterval = redeleteHangedFileInterval; + } + + public int getAccessMessageInMemoryMaxRatio() { + return accessMessageInMemoryMaxRatio; + } + + public void setAccessMessageInMemoryMaxRatio(int accessMessageInMemoryMaxRatio) { + this.accessMessageInMemoryMaxRatio = accessMessageInMemoryMaxRatio; + } + + public boolean isMessageIndexEnable() { + return messageIndexEnable; + } + + public void setMessageIndexEnable(boolean messageIndexEnable) { + this.messageIndexEnable = messageIndexEnable; + } + + public int getMaxHashSlotNum() { + return maxHashSlotNum; + } + + public void setMaxHashSlotNum(int maxHashSlotNum) { + this.maxHashSlotNum = maxHashSlotNum; + } + + public int getMaxIndexNum() { + return maxIndexNum; + } + + public void setMaxIndexNum(int maxIndexNum) { + this.maxIndexNum = maxIndexNum; + } + + public int getMaxMsgsNumBatch() { + return maxMsgsNumBatch; + } + + public void setMaxMsgsNumBatch(int maxMsgsNumBatch) { + this.maxMsgsNumBatch = maxMsgsNumBatch; + } + + public int getHaListenPort() { + return haListenPort; + } + + public void setHaListenPort(int haListenPort) { + if (haListenPort < 0) { + this.haListenPort = 0; + return; + } + this.haListenPort = haListenPort; + } + + public int getHaSendHeartbeatInterval() { + return haSendHeartbeatInterval; + } + + public void setHaSendHeartbeatInterval(int haSendHeartbeatInterval) { + this.haSendHeartbeatInterval = haSendHeartbeatInterval; + } + + public int getHaHousekeepingInterval() { + return haHousekeepingInterval; + } + + public void setHaHousekeepingInterval(int haHousekeepingInterval) { + this.haHousekeepingInterval = haHousekeepingInterval; + } + + public BrokerRole getBrokerRole() { + return brokerRole; + } + + public void setBrokerRole(BrokerRole brokerRole) { + this.brokerRole = brokerRole; + } + + public void setBrokerRole(String brokerRole) { + this.brokerRole = BrokerRole.valueOf(brokerRole); + } + + public int getHaTransferBatchSize() { + return haTransferBatchSize; + } + + public void setHaTransferBatchSize(int haTransferBatchSize) { + this.haTransferBatchSize = haTransferBatchSize; + } + + public int getHaMaxGapNotInSync() { + return haMaxGapNotInSync; + } + + public void setHaMaxGapNotInSync(int haMaxGapNotInSync) { + this.haMaxGapNotInSync = haMaxGapNotInSync; + } + + public FlushDiskType getFlushDiskType() { + return flushDiskType; + } + + public void setFlushDiskType(FlushDiskType flushDiskType) { + this.flushDiskType = flushDiskType; + } + + public void setFlushDiskType(String type) { + this.flushDiskType = FlushDiskType.valueOf(type); + } + + public int getSyncFlushTimeout() { + return syncFlushTimeout; + } + + 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; + } + + public void setHaMasterAddress(String haMasterAddress) { + this.haMasterAddress = haMasterAddress; + } + + public String getMessageDelayLevel() { + return messageDelayLevel; + } + + public void setMessageDelayLevel(String messageDelayLevel) { + this.messageDelayLevel = messageDelayLevel; + } + + public long getFlushDelayOffsetInterval() { + return flushDelayOffsetInterval; + } + + public void setFlushDelayOffsetInterval(long flushDelayOffsetInterval) { + this.flushDelayOffsetInterval = flushDelayOffsetInterval; + } + + public boolean isCleanFileForciblyEnable() { + return cleanFileForciblyEnable; + } + + public void setCleanFileForciblyEnable(boolean cleanFileForciblyEnable) { + this.cleanFileForciblyEnable = cleanFileForciblyEnable; + } + + public boolean isMessageIndexSafe() { + return messageIndexSafe; + } + + public void setMessageIndexSafe(boolean messageIndexSafe) { + this.messageIndexSafe = messageIndexSafe; + } + + public boolean isFlushCommitLogTimed() { + return flushCommitLogTimed; + } + + public void setFlushCommitLogTimed(boolean flushCommitLogTimed) { + this.flushCommitLogTimed = flushCommitLogTimed; + } + + public String getStorePathRootDir() { + return storePathRootDir; + } + + public void setStorePathRootDir(String storePathRootDir) { + this.storePathRootDir = storePathRootDir; + } + + public int getFlushLeastPagesWhenWarmMapedFile() { + return flushLeastPagesWhenWarmMapedFile; + } + + public void setFlushLeastPagesWhenWarmMapedFile(int flushLeastPagesWhenWarmMapedFile) { + this.flushLeastPagesWhenWarmMapedFile = flushLeastPagesWhenWarmMapedFile; + } + + public boolean isOffsetCheckInSlave() { + return offsetCheckInSlave; + } + + public void setOffsetCheckInSlave(boolean offsetCheckInSlave) { + this.offsetCheckInSlave = offsetCheckInSlave; + } + + public int getDefaultQueryMaxNum() { + return defaultQueryMaxNum; + } + + public void setDefaultQueryMaxNum(int defaultQueryMaxNum) { + this.defaultQueryMaxNum = defaultQueryMaxNum; + } + + public boolean isTransientStorePoolEnable() { + return transientStorePoolEnable; + } + + public void setTransientStorePoolEnable(final boolean transientStorePoolEnable) { + this.transientStorePoolEnable = transientStorePoolEnable; + } + + public int getTransientStorePoolSize() { + return transientStorePoolSize; + } + + public void setTransientStorePoolSize(final int transientStorePoolSize) { + this.transientStorePoolSize = transientStorePoolSize; + } + + public int getCommitIntervalCommitLog() { + return commitIntervalCommitLog; + } + + public void setCommitIntervalCommitLog(final int commitIntervalCommitLog) { + this.commitIntervalCommitLog = commitIntervalCommitLog; + } + + public boolean isFastFailIfNoBufferInStorePool() { + return fastFailIfNoBufferInStorePool; + } + + public void setFastFailIfNoBufferInStorePool(final boolean fastFailIfNoBufferInStorePool) { + this.fastFailIfNoBufferInStorePool = fastFailIfNoBufferInStorePool; + } + + public boolean isUseReentrantLockWhenPutMessage() { + return useReentrantLockWhenPutMessage; + } + + public void setUseReentrantLockWhenPutMessage(final boolean useReentrantLockWhenPutMessage) { + this.useReentrantLockWhenPutMessage = useReentrantLockWhenPutMessage; + } + + public int getCommitCommitLogLeastPages() { + return commitCommitLogLeastPages; + } + + public void setCommitCommitLogLeastPages(final int commitCommitLogLeastPages) { + this.commitCommitLogLeastPages = commitCommitLogLeastPages; + } + + public int getCommitCommitLogThoroughInterval() { + return commitCommitLogThoroughInterval; + } + + public void setCommitCommitLogThoroughInterval(final int commitCommitLogThoroughInterval) { + 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 isTimerEnableRetryUntilSuccess() { + return timerEnableRetryUntilSuccess; + } + + public void setTimerEnableRetryUntilSuccess(boolean timerEnableRetryUntilSuccess) { + this.timerEnableRetryUntilSuccess = timerEnableRetryUntilSuccess; + } + + 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; + } + + public boolean isReadUnCommitted() { + return readUnCommitted; + } + + public void setReadUnCommitted(boolean readUnCommitted) { + this.readUnCommitted = readUnCommitted; + } + + public boolean isPutConsumeQueueDataByFileChannel() { + return putConsumeQueueDataByFileChannel; + } + + public void setPutConsumeQueueDataByFileChannel(boolean putConsumeQueueDataByFileChannel) { + this.putConsumeQueueDataByFileChannel = putConsumeQueueDataByFileChannel; + } + + public String getBottomMostCompressionTypeForConsumeQueueStore() { + return bottomMostCompressionTypeForConsumeQueueStore; + } + + public void setBottomMostCompressionTypeForConsumeQueueStore(String bottomMostCompressionTypeForConsumeQueueStore) { + this.bottomMostCompressionTypeForConsumeQueueStore = bottomMostCompressionTypeForConsumeQueueStore; + } + + public int getRocksdbFlushWalFrequency() { + return rocksdbFlushWalFrequency; + } + + public void setRocksdbFlushWalFrequency(int rocksdbFlushWalFrequency) { + this.rocksdbFlushWalFrequency = rocksdbFlushWalFrequency; + } + + public long getRocksdbWalFileRollingThreshold() { + return rocksdbWalFileRollingThreshold; + } + + public void setRocksdbWalFileRollingThreshold(long rocksdbWalFileRollingThreshold) { + this.rocksdbWalFileRollingThreshold = rocksdbWalFileRollingThreshold; + } + + public int getSpinLockCollisionRetreatOptimalDegree() { + return spinLockCollisionRetreatOptimalDegree; + } + + public void setSpinLockCollisionRetreatOptimalDegree(int spinLockCollisionRetreatOptimalDegree) { + this.spinLockCollisionRetreatOptimalDegree = spinLockCollisionRetreatOptimalDegree; + } + + public void setUseABSLock(boolean useABSLock) { + this.useABSLock = useABSLock; + } + + public boolean getUseABSLock() { + return useABSLock; + } +} 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 new file mode 100644 index 0000000..2f34e7d --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/config/StorePathConfigHelper.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.config; + +import java.io.File; + +public class StorePathConfigHelper { + + public static String getStorePathConsumeQueue(final String rootDir) { + return rootDir + File.separator + "consumequeue"; + } + + 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"; + } + + public static String getStoreCheckpoint(final String rootDir) { + return rootDir + File.separator + "checkpoint"; + } + + public static String getAbortFile(final String rootDir) { + return rootDir + File.separator + "abort"; + } + + public static String getLockFile(final String rootDir) { + return rootDir + File.separator + "lock"; + } + + public static String getDelayOffsetStorePath(final String rootDir) { + return rootDir + File.separator + "config" + File.separator + "delayOffset.json"; + } + + public static String getTranStateTableStorePath(final String rootDir) { + return rootDir + File.separator + "transaction" + File.separator + "statetable"; + } + + public static String getTranRedoLogStorePath(final String rootDir) { + return rootDir + File.separator + "transaction" + File.separator + "redolog"; + } + +} 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 0000000..29be9e7 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/dledger/DLedgerCommitLog.java @@ -0,0 +1,1150 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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; +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; + +/** + * 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(); + boolean checkDupInfo = this.defaultMessageStore.getMessageStoreConfig().isDuplicationEnable(); + 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, checkDupInfo); + 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) throws RocksDBException { + ByteBuffer byteBuffer = mmapFile.sliceByteBuffer(); + + int magicCode = byteBuffer.getInt(DLedgerEntry.BODY_OFFSET + MessageDecoder.MESSAGE_MAGIC_CODE_POSITION); + if (magicCode != MESSAGE_MAGIC_CODE) { + return false; + } + + if (this.defaultMessageStore.getMessageStoreConfig().isEnableRocksDBStore()) { + final long maxPhyOffsetInConsumeQueue = this.defaultMessageStore.getQueueStore().getMaxPhyOffsetInConsumeQueue(); + long phyOffset = byteBuffer.getLong(DLedgerEntry.BODY_OFFSET + MessageDecoder.MESSAGE_PHYSIC_OFFSET_POSITION); + if (phyOffset <= maxPhyOffsetInConsumeQueue) { + log.info("find check. beginPhyOffset: {}, maxPhyOffsetInConsumeQueue: {}", phyOffset, maxPhyOffsetInConsumeQueue); + return true; + } + } else { + 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; + + encodeResult = this.messageSerializer.serialize(msg); + if (encodeResult.status != AppendMessageStatus.PUT_OK) { + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, new AppendMessageResult(encodeResult.status))); + } + + String topicQueueKey = msg.getTopic() + "-" + msg.getQueueId(); + topicQueueLock.lock(topicQueueKey); + try { + defaultMessageStore.assignOffset(msg); + + 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/exception/ConsumeQueueException.java b/store/src/main/java/org/apache/rocketmq/store/exception/ConsumeQueueException.java new file mode 100644 index 0000000..880e634 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/exception/ConsumeQueueException.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.store.exception; + +public class ConsumeQueueException extends StoreException { + public ConsumeQueueException() { + } + + public ConsumeQueueException(String message) { + super(message); + } + + public ConsumeQueueException(String message, Throwable cause) { + super(message, cause); + } + + public ConsumeQueueException(Throwable cause) { + super(cause); + } + + public ConsumeQueueException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/exception/StoreException.java b/store/src/main/java/org/apache/rocketmq/store/exception/StoreException.java new file mode 100644 index 0000000..8c99e8a --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/exception/StoreException.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.store.exception; + +public class StoreException extends Exception { + public StoreException() { + } + + public StoreException(String message) { + super(message); + } + + public StoreException(String message, Throwable cause) { + super(message, cause); + } + + public StoreException(Throwable cause) { + super(cause); + } + + public StoreException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} 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 0000000..530d295 --- /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 0000000..5dd2441 --- /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 0000000..c0e2038 --- /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); + DefaultHAService.this.addConnection(conn); + conn.start(); + } 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 0000000..810f286 --- /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 0000000..a75cae8 --- /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 0000000..0449e01 --- /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 new file mode 100644 index 0000000..8e1e922 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ha/HAConnection.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.ha; + +import java.nio.channels.SocketChannel; + +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 0000000..4f0c5ca --- /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 0000000..8a3f6aa --- /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 0000000..197d9f6 --- /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 new file mode 100644 index 0000000..aaea7d6 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ha/HAService.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.ha; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +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.MessageStoreConfig; +import org.rocksdb.RocksDBException; + +public interface HAService { + + /** + * Init HAService, must be called before other methods. + * + * @param defaultMessageStore + * @throws IOException + */ + void init(DefaultMessageStore defaultMessageStore) throws IOException; + + /** + * Start HA Service + * + * @throws Exception + */ + void start() throws Exception; + + /** + * Shutdown HA Service + */ + void shutdown(); + + /** + * Change to master state + * + * @param masterEpoch the new masterEpoch + */ + default boolean changeToMaster(int masterEpoch) throws RocksDBException { + return false; + } + + /** + * Change to master state + * + * @param masterEpoch the new masterEpoch + */ + default boolean changeToMasterWhenLastRoleIsMaster(int masterEpoch) { + return false; + } + + /** + * Change to slave state + * + * @param newMasterAddr new master addr + * @param newMasterEpoch new masterEpoch + */ + default boolean changeToSlave(String newMasterAddr, int newMasterEpoch, Long slaveId) { + return false; + } + + /** + * Change to slave state + * + * @param newMasterAddr new master addr + * @param newMasterEpoch new masterEpoch + */ + default boolean changeToSlaveWhenMasterNotChange(String newMasterAddr, int newMasterEpoch) { + return false; + } + + /** + * Update master address + * + * @param newAddr + */ + void updateMasterAddress(String newAddr); + + /** + * Update ha master address + * + * @param newAddr + */ + void updateHaMasterAddress(String newAddr); + + /** + * 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); + + /** + * Get connection count + * + * @return the number of connection + */ + AtomicInteger getConnectionCount(); + + /** + * Put request to handle HA + * + * @param request + */ + void putRequest(final CommitLog.GroupCommitRequest request); + + /** + * Put GroupConnectionStateRequest for preOnline + * + * @param request + */ + void putGroupConnectionStateRequest(HAConnectionStateNotificationRequest request); + + /** + * Get ha connection list + * + * @return List + */ + List getConnectionList(); + + /** + * Get HAClient + * + * @return HAClient + */ + HAClient getHAClient(); + + /** + * Get the max offset in all slaves + */ + AtomicLong getPush2SlaveMaxOffset(); + + /** + * Get HA runtime info + */ + HARuntimeInfo getRuntimeInfo(final long masterPutWhere); + + /** + * Get WaitNotifyObject + */ + WaitNotifyObject getWaitNotifyObject(); + + /** + * 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 new file mode 100644 index 0000000..c040bf9 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ha/WaitNotifyObject.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.store.ha; + +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; + +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 ConcurrentHashMap waitingThreadTable = + new ConcurrentHashMap<>(16); + + protected AtomicBoolean hasNotified = new AtomicBoolean(false); + + public void wakeup() { + 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) { + try { + if (this.hasNotified.compareAndSet(true, false)) { + this.onWaitEnd(); + return; + } + this.wait(interval); + } catch (InterruptedException e) { + log.error("Interrupted", e); + } finally { + this.hasNotified.set(false); + this.onWaitEnd(); + } + } + } + + protected void onWaitEnd() { + } + + public void wakeupAll() { + boolean needNotify = false; + for (Map.Entry entry : this.waitingThreadTable.entrySet()) { + if (entry.getValue().compareAndSet(false, true)) { + needNotify = true; + } + } + if (needNotify) { + synchronized (this) { + this.notifyAll(); + } + } + } + + 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) { + try { + if (notified.compareAndSet(true, false)) { + this.onWaitEnd(); + return; + } + this.wait(interval); + } catch (InterruptedException e) { + log.error("Interrupted", e); + } finally { + 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 0000000..176c25a --- /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 0000000..440cd3c --- /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 0000000..64dad9a --- /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 0000000..eb6ab63 --- /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 0000000..f23e4aa --- /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 0000000..e89aedb --- /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 0000000..7a4126b --- /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 0000000..b71e216 --- /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 0000000..4efcadd --- /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 0000000..9594328 --- /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 0000000..0f5699b --- /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 0000000..dc47d32 --- /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 0000000..0225450 --- /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 new file mode 100644 index 0000000..9e0669f --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/index/IndexFile.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.store.index; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.util.List; +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.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 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 { + this.fileTotalSize = + IndexHeader.INDEX_HEADER_SIZE + (hashSlotNum * hashSlotSize) + (indexNum * indexSize); + this.mappedFile = new DefaultMappedFile(fileName, fileTotalSize); + this.mappedByteBuffer = this.mappedFile.getMappedByteBuffer(); + this.hashSlotNum = hashSlotNum; + this.indexNum = indexNum; + + ByteBuffer byteBuffer = this.mappedByteBuffer.slice(); + this.indexHeader = new IndexHeader(byteBuffer); + + if (endPhyOffset > 0) { + this.indexHeader.setBeginPhyOffset(endPhyOffset); + this.indexHeader.setEndPhyOffset(endPhyOffset); + } + + if (endTimestamp > 0) { + this.indexHeader.setBeginTimestamp(endTimestamp); + this.indexHeader.setEndTimestamp(endTimestamp); + } + } + + 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 elapsed time(ms) " + (System.currentTimeMillis() - beginTime)); + } + } + + public boolean isWriteFull() { + return this.indexHeader.getIndexCount() >= this.indexNum; + } + + public boolean destroy(final long intervalForcibly) { + return this.mappedFile.destroy(intervalForcibly); + } + + public boolean putKey(final String key, final long phyOffset, final long storeTimestamp) { + if (this.indexHeader.getIndexCount() < this.indexNum) { + int keyHash = indexKeyHashMethod(key); + int slotPos = keyHash % this.hashSlotNum; + int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize; + + try { + + int slotValue = this.mappedByteBuffer.getInt(absSlotPos); + if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount()) { + slotValue = invalidIndex; + } + + long timeDiff = storeTimestamp - this.indexHeader.getBeginTimestamp(); + + timeDiff = timeDiff / 1000; + + if (this.indexHeader.getBeginTimestamp() <= 0) { + timeDiff = 0; + } else if (timeDiff > Integer.MAX_VALUE) { + timeDiff = Integer.MAX_VALUE; + } else if (timeDiff < 0) { + timeDiff = 0; + } + + int absIndexPos = + IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize + + this.indexHeader.getIndexCount() * indexSize; + + this.mappedByteBuffer.putInt(absIndexPos, keyHash); + this.mappedByteBuffer.putLong(absIndexPos + 4, phyOffset); + this.mappedByteBuffer.putInt(absIndexPos + 4 + 8, (int) timeDiff); + this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue); + + this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount()); + + if (this.indexHeader.getIndexCount() <= 1) { + this.indexHeader.setBeginPhyOffset(phyOffset); + this.indexHeader.setBeginTimestamp(storeTimestamp); + } + + if (invalidIndex == slotValue) { + this.indexHeader.incHashSlotCount(); + } + this.indexHeader.incIndexCount(); + this.indexHeader.setEndPhyOffset(phyOffset); + this.indexHeader.setEndTimestamp(storeTimestamp); + + return true; + } catch (Exception e) { + log.error("putKey exception, Key: " + key + " KeyHashCode: " + key.hashCode(), e); + } + } else { + log.warn("Over index file capacity: index count = " + this.indexHeader.getIndexCount() + + "; index max num = " + this.indexNum); + } + + return false; + } + + public int indexKeyHashMethod(final String key) { + int keyHash = key.hashCode(); + int keyHashPositive = Math.abs(keyHash); + if (keyHashPositive < 0) { + keyHashPositive = 0; + } + return keyHashPositive; + } + + public long getBeginTimestamp() { + return this.indexHeader.getBeginTimestamp(); + } + + public long getEndTimestamp() { + return this.indexHeader.getEndTimestamp(); + } + + public long getEndPhyOffset() { + return this.indexHeader.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(); + return result; + } + + public void selectPhyOffset(final List phyOffsets, final String key, final int maxNum, + 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; + + try { + int slotValue = this.mappedByteBuffer.getInt(absSlotPos); + if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount() + || this.indexHeader.getIndexCount() <= 1) { + } else { + for (int nextIndexToRead = slotValue; ; ) { + if (phyOffsets.size() >= maxNum) { + break; + } + + int absIndexPos = + IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize + + nextIndexToRead * indexSize; + + int keyHashRead = this.mappedByteBuffer.getInt(absIndexPos); + long phyOffsetRead = this.mappedByteBuffer.getLong(absIndexPos + 4); + + long timeDiff = this.mappedByteBuffer.getInt(absIndexPos + 4 + 8); + int prevIndexRead = this.mappedByteBuffer.getInt(absIndexPos + 4 + 8 + 4); + + if (timeDiff < 0) { + break; + } + + timeDiff *= 1000L; + + long timeRead = this.indexHeader.getBeginTimestamp() + timeDiff; + boolean timeMatched = timeRead >= begin && timeRead <= end; + + if (keyHash == keyHashRead && timeMatched) { + phyOffsets.add(phyOffsetRead); + } + + if (prevIndexRead <= invalidIndex + || prevIndexRead > this.indexHeader.getIndexCount() + || prevIndexRead == nextIndexToRead || timeRead < begin) { + break; + } + + nextIndexToRead = prevIndexRead; + } + } + } catch (Exception e) { + log.error("selectPhyOffset exception ", e); + } finally { + 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 new file mode 100644 index 0000000..fe319ca --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/index/IndexHeader.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.index; + +import java.nio.ByteBuffer; +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; + private static int endTimestampIndex = 8; + private static int beginPhyoffsetIndex = 16; + private static int endPhyoffsetIndex = 24; + private static int hashSlotcountIndex = 32; + private static int indexCountIndex = 36; + private final ByteBuffer byteBuffer; + 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; + } + + public void load() { + this.beginTimestamp.set(byteBuffer.getLong(beginTimestampIndex)); + this.endTimestamp.set(byteBuffer.getLong(endTimestampIndex)); + this.beginPhyOffset.set(byteBuffer.getLong(beginPhyoffsetIndex)); + this.endPhyOffset.set(byteBuffer.getLong(endPhyoffsetIndex)); + + this.hashSlotCount.set(byteBuffer.getInt(hashSlotcountIndex)); + this.indexCount.set(byteBuffer.getInt(indexCountIndex)); + + if (this.indexCount.get() <= 0) { + this.indexCount.set(1); + } + } + + public void updateByteBuffer() { + this.byteBuffer.putLong(beginTimestampIndex, this.beginTimestamp.get()); + this.byteBuffer.putLong(endTimestampIndex, this.endTimestamp.get()); + this.byteBuffer.putLong(beginPhyoffsetIndex, this.beginPhyOffset.get()); + this.byteBuffer.putLong(endPhyoffsetIndex, this.endPhyOffset.get()); + this.byteBuffer.putInt(hashSlotcountIndex, this.hashSlotCount.get()); + this.byteBuffer.putInt(indexCountIndex, this.indexCount.get()); + } + + public long getBeginTimestamp() { + return beginTimestamp.get(); + } + + public void setBeginTimestamp(long beginTimestamp) { + this.beginTimestamp.set(beginTimestamp); + this.byteBuffer.putLong(beginTimestampIndex, beginTimestamp); + } + + public long getEndTimestamp() { + return endTimestamp.get(); + } + + public void setEndTimestamp(long endTimestamp) { + this.endTimestamp.set(endTimestamp); + this.byteBuffer.putLong(endTimestampIndex, endTimestamp); + } + + public long getBeginPhyOffset() { + return beginPhyOffset.get(); + } + + public void setBeginPhyOffset(long beginPhyOffset) { + this.beginPhyOffset.set(beginPhyOffset); + this.byteBuffer.putLong(beginPhyoffsetIndex, beginPhyOffset); + } + + public long getEndPhyOffset() { + return endPhyOffset.get(); + } + + public void setEndPhyOffset(long endPhyOffset) { + this.endPhyOffset.set(endPhyOffset); + this.byteBuffer.putLong(endPhyoffsetIndex, endPhyOffset); + } + + public AtomicInteger getHashSlotCount() { + return hashSlotCount; + } + + public void incHashSlotCount() { + int value = this.hashSlotCount.incrementAndGet(); + this.byteBuffer.putInt(hashSlotcountIndex, value); + } + + public int getIndexCount() { + return indexCount.get(); + } + + public void incIndexCount() { + int value = this.indexCount.incrementAndGet(); + this.byteBuffer.putInt(indexCountIndex, value); + } +} 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 new file mode 100644 index 0000000..2d325ee --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/index/IndexService.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.index; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +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; + +public class IndexService { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + /** + * Maximum times to attempt index file creation. + */ + private static final int MAX_TRY_IDX_CREATE = 3; + private final DefaultMessageStore defaultMessageStore; + private final int hashSlotNum; + private final int indexNum; + private final String storePath; + private final ArrayList indexFileList = new ArrayList<>(); + private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + + public IndexService(final DefaultMessageStore store) { + this.defaultMessageStore = store; + this.hashSlotNum = store.getMessageStoreConfig().getMaxHashSlotNum(); + this.indexNum = store.getMessageStoreConfig().getMaxIndexNum(); + this.storePath = + StorePathConfigHelper.getStorePathIndex(defaultMessageStore.getMessageStoreConfig().getStorePathRootDir()); + } + + public boolean load(final boolean lastExitOK) { + File dir = new File(this.storePath); + File[] files = dir.listFiles(); + if (files != null) { + // ascending order + Arrays.sort(files); + for (File file : files) { + try { + IndexFile f = new IndexFile(file.getPath(), this.hashSlotNum, this.indexNum, 0, 0); + f.load(); + + if (!lastExitOK) { + if (f.getEndTimestamp() > this.defaultMessageStore.getStoreCheckpoint() + .getIndexMsgTimestamp()) { + f.destroy(0); + continue; + } + } + + LOGGER.info("load index file OK, " + f.getFileName()); + this.indexFileList.add(f); + } catch (IOException e) { + LOGGER.error("load file {} error", file, e); + return false; + } catch (NumberFormatException e) { + LOGGER.error("load file {} error", file, e); + } + } + } + + 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 { + this.readWriteLock.readLock().lock(); + if (this.indexFileList.isEmpty()) { + return; + } + + long endPhyOffset = this.indexFileList.get(0).getEndPhyOffset(); + if (endPhyOffset < offset) { + files = this.indexFileList.toArray(); + } + } catch (Exception e) { + LOGGER.error("destroy exception", e); + } finally { + this.readWriteLock.readLock().unlock(); + } + + if (files != null) { + List fileList = new ArrayList<>(); + for (int i = 0; i < (files.length - 1); i++) { + IndexFile f = (IndexFile) files[i]; + if (f.getEndPhyOffset() < offset) { + fileList.add(f); + } else { + break; + } + } + + this.deleteExpiredFile(fileList); + } + } + + private void deleteExpiredFile(List files) { + if (!files.isEmpty()) { + try { + this.readWriteLock.writeLock().lock(); + for (IndexFile file : files) { + boolean destroyed = file.destroy(3000); + destroyed = destroyed && this.indexFileList.remove(file); + if (!destroyed) { + LOGGER.error("deleteExpiredFile remove failed."); + break; + } + } + } catch (Exception e) { + LOGGER.error("deleteExpiredFile has exception.", e); + } finally { + this.readWriteLock.writeLock().unlock(); + } + } + } + + public void destroy() { + try { + this.readWriteLock.writeLock().lock(); + for (IndexFile f : this.indexFileList) { + f.destroy(1000 * 3); + } + this.indexFileList.clear(); + } catch (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) { + long indexLastUpdateTimestamp = 0; + long indexLastUpdatePhyoffset = 0; + maxNum = Math.min(maxNum, this.defaultMessageStore.getMessageStoreConfig().getMaxMsgsNumBatch()); + List phyOffsets = new ArrayList<>(maxNum); + try { + this.readWriteLock.readLock().lock(); + if (!this.indexFileList.isEmpty()) { + for (int i = this.indexFileList.size(); i > 0; i--) { + IndexFile f = this.indexFileList.get(i - 1); + boolean lastFile = i == this.indexFileList.size(); + if (lastFile) { + indexLastUpdateTimestamp = f.getEndTimestamp(); + indexLastUpdatePhyoffset = f.getEndPhyOffset(); + } + + if (f.isTimeMatched(begin, end)) { + + f.selectPhyOffset(phyOffsets, buildKey(topic, key), maxNum, begin, end); + } + + if (f.getBeginTimestamp() < begin) { + break; + } + + if (phyOffsets.size() >= maxNum) { + break; + } + } + } + } catch (Exception e) { + LOGGER.error("queryMsg exception", e); + } finally { + this.readWriteLock.readLock().unlock(); + } + + return new QueryOffsetResult(phyOffsets, indexLastUpdateTimestamp, indexLastUpdatePhyoffset); + } + + private String buildKey(final String topic, final String key) { + return topic + "#" + key; + } + + public void buildIndex(DispatchRequest req) { + IndexFile indexFile = retryGetAndCreateIndexFile(); + if (indexFile != null) { + long endPhyOffset = indexFile.getEndPhyOffset(); + DispatchRequest msg = req; + String topic = msg.getTopic(); + String keys = msg.getKeys(); + if (msg.getCommitLogOffset() < endPhyOffset) { + return; + } + + final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag()); + switch (tranType) { + case MessageSysFlag.TRANSACTION_NOT_TYPE: + case MessageSysFlag.TRANSACTION_PREPARED_TYPE: + case MessageSysFlag.TRANSACTION_COMMIT_TYPE: + break; + case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE: + return; + } + + if (req.getUniqKey() != null) { + indexFile = putKey(indexFile, msg, buildKey(topic, req.getUniqKey())); + if (indexFile == null) { + LOGGER.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey()); + return; + } + } + + if (keys != null && keys.length() > 0) { + String[] keyset = keys.split(MessageConst.KEY_SEPARATOR); + for (int i = 0; i < keyset.length; i++) { + String key = keyset[i]; + if (key.length() > 0) { + indexFile = putKey(indexFile, msg, buildKey(topic, key)); + if (indexFile == null) { + LOGGER.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey()); + return; + } + } + } + } + } else { + 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; ) { + LOGGER.warn("Index file [" + indexFile.getFileName() + "] is full, trying to create another one"); + + indexFile = retryGetAndCreateIndexFile(); + if (null == indexFile) { + return null; + } + + ok = indexFile.putKey(idxKey, msg.getCommitLogOffset(), msg.getStoreTimestamp()); + } + + return indexFile; + } + + /** + * Retries to get or create index file. + * + * @return {@link IndexFile} or null on failure. + */ + public IndexFile retryGetAndCreateIndexFile() { + IndexFile indexFile = null; + + for (int times = 0; null == indexFile && times < MAX_TRY_IDX_CREATE; times++) { + indexFile = this.getAndCreateLastIndexFile(); + if (null != indexFile) { + break; + } + + try { + LOGGER.info("Tried to create index file " + times + " times"); + Thread.sleep(1000); + } catch (InterruptedException e) { + LOGGER.error("Interrupted", e); + } + } + + if (null == indexFile) { + this.defaultMessageStore.getRunningFlags().makeIndexFileError(); + LOGGER.error("Mark index file cannot build flag"); + } + + return indexFile; + } + + public IndexFile getAndCreateLastIndexFile() { + IndexFile indexFile = null; + IndexFile prevIndexFile = null; + long lastUpdateEndPhyOffset = 0; + long lastUpdateIndexTimestamp = 0; + + { + this.readWriteLock.readLock().lock(); + if (!this.indexFileList.isEmpty()) { + IndexFile tmp = this.indexFileList.get(this.indexFileList.size() - 1); + if (!tmp.isWriteFull()) { + indexFile = tmp; + } else { + lastUpdateEndPhyOffset = tmp.getEndPhyOffset(); + lastUpdateIndexTimestamp = tmp.getEndTimestamp(); + prevIndexFile = tmp; + } + } + + this.readWriteLock.readLock().unlock(); + } + + if (indexFile == null) { + try { + String fileName = + this.storePath + File.separator + + UtilAll.timeMillisToHumanString(System.currentTimeMillis()); + indexFile = + new IndexFile(fileName, this.hashSlotNum, this.indexNum, lastUpdateEndPhyOffset, + lastUpdateIndexTimestamp); + this.readWriteLock.writeLock().lock(); + this.indexFileList.add(indexFile); + } catch (Exception e) { + LOGGER.error("getLastIndexFile exception ", e); + } finally { + this.readWriteLock.writeLock().unlock(); + } + + if (indexFile != null) { + final IndexFile flushThisFile = prevIndexFile; + + Thread flushThread = new Thread(new AbstractBrokerRunnable(defaultMessageStore.getBrokerConfig()) { + @Override + public void run0() { + IndexService.this.flush(flushThisFile); + } + }, "FlushIndexFileThread"); + + flushThread.setDaemon(true); + flushThread.start(); + } + } + + return indexFile; + } + + public void flush(final IndexFile f) { + if (null == f) { + return; + } + + long indexMsgTimestamp = 0; + + if (f.isWriteFull()) { + indexMsgTimestamp = f.getEndTimestamp(); + } + + f.flush(); + + if (indexMsgTimestamp > 0) { + this.defaultMessageStore.getStoreCheckpoint().setIndexMsgTimestamp(indexMsgTimestamp); + this.defaultMessageStore.getStoreCheckpoint().flush(); + } + } + + 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/index/QueryOffsetResult.java b/store/src/main/java/org/apache/rocketmq/store/index/QueryOffsetResult.java new file mode 100644 index 0000000..72cb913 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/index/QueryOffsetResult.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.index; + +import java.util.List; + +public class QueryOffsetResult { + private final List phyOffsets; + private final long indexLastUpdateTimestamp; + private final long indexLastUpdatePhyoffset; + + public QueryOffsetResult(List phyOffsets, long indexLastUpdateTimestamp, + long indexLastUpdatePhyoffset) { + this.phyOffsets = phyOffsets; + this.indexLastUpdateTimestamp = indexLastUpdateTimestamp; + this.indexLastUpdatePhyoffset = indexLastUpdatePhyoffset; + } + + public List getPhyOffsets() { + return phyOffsets; + } + + public long getIndexLastUpdateTimestamp() { + return indexLastUpdateTimestamp; + } + + public long getIndexLastUpdatePhyoffset() { + return indexLastUpdatePhyoffset; + } +} 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 0000000..5c285b1 --- /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 0000000..be2bb55 --- /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 0000000..4181b34 --- /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 0000000..5e07a50 --- /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 0000000..639084f --- /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 0000000..183f066 --- /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/lock/AdaptiveBackOffSpinLock.java b/store/src/main/java/org/apache/rocketmq/store/lock/AdaptiveBackOffSpinLock.java new file mode 100644 index 0000000..96200bc --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/lock/AdaptiveBackOffSpinLock.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.lock; + +import org.apache.rocketmq.store.PutMessageLock; +import org.apache.rocketmq.store.config.MessageStoreConfig; + +public interface AdaptiveBackOffSpinLock extends PutMessageLock { + /** + * Configuration update + * @param messageStoreConfig + */ + default void update(MessageStoreConfig messageStoreConfig) { + } + + /** + * Locking mechanism switching + */ + default void swap() { + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/lock/AdaptiveBackOffSpinLockImpl.java b/store/src/main/java/org/apache/rocketmq/store/lock/AdaptiveBackOffSpinLockImpl.java new file mode 100644 index 0000000..b4abb08 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/lock/AdaptiveBackOffSpinLockImpl.java @@ -0,0 +1,207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.lock; + +import org.apache.rocketmq.store.config.MessageStoreConfig; + +import java.time.LocalTime; +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.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +public class AdaptiveBackOffSpinLockImpl implements AdaptiveBackOffSpinLock { + private AdaptiveBackOffSpinLock adaptiveLock; + //state + private AtomicBoolean state = new AtomicBoolean(true); + + // Used to determine the switchover between a mutex lock and a spin lock + private final static float SWAP_SPIN_LOCK_RATIO = 0.8f; + + // It is used to adjust the spin number K of the escape spin lock + // When (retreat number / TPS) <= (1 / BASE_SWAP_ADAPTIVE_RATIO * SPIN_LOCK_ADAPTIVE_RATIO), K is decreased + private final static int SPIN_LOCK_ADAPTIVE_RATIO = 4; + + // It is used to adjust the spin number K of the escape spin lock + // When (retreat number / TPS) >= (1 / BASE_SWAP_ADAPTIVE_RATIO), K is increased + private final static int BASE_SWAP_LOCK_RATIO = 320; + + private final static String BACK_OFF_SPIN_LOCK = "SpinLock"; + + private final static String REENTRANT_LOCK = "ReentrantLock"; + + private Map locks; + + private final List tpsTable; + + private final List> threadTable; + + private int swapCriticalPoint; + + private AtomicInteger currentThreadNum = new AtomicInteger(0); + + private AtomicBoolean isOpen = new AtomicBoolean(true); + + public AdaptiveBackOffSpinLockImpl() { + this.locks = new HashMap<>(); + this.locks.put(REENTRANT_LOCK, new BackOffReentrantLock()); + this.locks.put(BACK_OFF_SPIN_LOCK, new BackOffSpinLock()); + + this.threadTable = new ArrayList<>(2); + this.threadTable.add(new ConcurrentHashMap<>()); + this.threadTable.add(new ConcurrentHashMap<>()); + + this.tpsTable = new ArrayList<>(2); + this.tpsTable.add(new AtomicInteger(0)); + this.tpsTable.add(new AtomicInteger(0)); + + adaptiveLock = this.locks.get(BACK_OFF_SPIN_LOCK); + } + + @Override + public void lock() { + int slot = LocalTime.now().getSecond() % 2; + this.threadTable.get(slot).putIfAbsent(Thread.currentThread(), Byte.MAX_VALUE); + this.tpsTable.get(slot).getAndIncrement(); + boolean state; + do { + state = this.state.get(); + } while (!state); + + currentThreadNum.incrementAndGet(); + this.adaptiveLock.lock(); + } + + @Override + public void unlock() { + this.adaptiveLock.unlock(); + currentThreadNum.decrementAndGet(); + if (isOpen.get()) { + swap(); + } + } + + @Override + public void update(MessageStoreConfig messageStoreConfig) { + this.adaptiveLock.update(messageStoreConfig); + } + + @Override + public void swap() { + if (!this.state.get()) { + return; + } + boolean needSwap = false; + int slot = 1 - LocalTime.now().getSecond() % 2; + int tps = this.tpsTable.get(slot).get() + 1; + int threadNum = this.threadTable.get(slot).size(); + this.tpsTable.get(slot).set(-1); + this.threadTable.get(slot).clear(); + if (tps == 0) { + return; + } + + if (this.adaptiveLock instanceof BackOffSpinLock) { + BackOffSpinLock lock = (BackOffSpinLock) this.adaptiveLock; + // Avoid frequent adjustment of K, and make a reasonable range through experiments + // reasonable range : (retreat number / TPS) > (1 / BASE_SWAP_ADAPTIVE_RATIO * SPIN_LOCK_ADAPTIVE_RATIO) && + // (retreat number / TPS) < (1 / BASE_SWAP_ADAPTIVE_RATIO) + if (lock.getNumberOfRetreat(slot) * BASE_SWAP_LOCK_RATIO >= tps) { + if (lock.isAdapt()) { + lock.adapt(true); + } else { + // It is used to switch between mutex lock and spin lock + this.swapCriticalPoint = tps * threadNum; + needSwap = true; + } + } else if (lock.getNumberOfRetreat(slot) * BASE_SWAP_LOCK_RATIO * SPIN_LOCK_ADAPTIVE_RATIO <= tps) { + lock.adapt(false); + } + lock.setNumberOfRetreat(slot, 0); + } else { + if (tps * threadNum <= this.swapCriticalPoint * SWAP_SPIN_LOCK_RATIO) { + needSwap = true; + } + } + + if (needSwap) { + if (this.state.compareAndSet(true, false)) { + // Ensures that no threads are in contention locks as well as in critical zones + int currentThreadNum; + do { + currentThreadNum = this.currentThreadNum.get(); + } while (currentThreadNum != 0); + + try { + if (this.adaptiveLock instanceof BackOffSpinLock) { + this.adaptiveLock = this.locks.get(REENTRANT_LOCK); + } else { + this.adaptiveLock = this.locks.get(BACK_OFF_SPIN_LOCK); + ((BackOffSpinLock) this.adaptiveLock).adapt(false); + } + } catch (Exception e) { + //ignore + } finally { + this.state.compareAndSet(false, true); + } + } + } + } + + public List getLocks() { + return (List) this.locks.values(); + } + + public void setLocks(Map locks) { + this.locks = locks; + } + + public boolean getState() { + return this.state.get(); + } + + public void setState(boolean state) { + this.state.set(state); + } + + public AdaptiveBackOffSpinLock getAdaptiveLock() { + return adaptiveLock; + } + + public List getTpsTable() { + return tpsTable; + } + + public void setSwapCriticalPoint(int swapCriticalPoint) { + this.swapCriticalPoint = swapCriticalPoint; + } + + public int getSwapCriticalPoint() { + return swapCriticalPoint; + } + + public boolean isOpen() { + return this.isOpen.get(); + } + + public void setOpen(boolean open) { + this.isOpen.set(open); + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/lock/BackOffReentrantLock.java b/store/src/main/java/org/apache/rocketmq/store/lock/BackOffReentrantLock.java new file mode 100644 index 0000000..90e4164 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/lock/BackOffReentrantLock.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.lock; + +import java.util.concurrent.locks.ReentrantLock; + +public class BackOffReentrantLock implements AdaptiveBackOffSpinLock { + private ReentrantLock putMessageNormalLock = new ReentrantLock(); // NonfairSync + + @Override + public void lock() { + putMessageNormalLock.lock(); + } + + @Override + public void unlock() { + putMessageNormalLock.unlock(); + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/lock/BackOffSpinLock.java b/store/src/main/java/org/apache/rocketmq/store/lock/BackOffSpinLock.java new file mode 100644 index 0000000..f754970 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/lock/BackOffSpinLock.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.lock; + +import org.apache.rocketmq.store.config.MessageStoreConfig; + +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +public class BackOffSpinLock implements AdaptiveBackOffSpinLock { + + private AtomicBoolean putMessageSpinLock = new AtomicBoolean(true); + + private int optimalDegree; + + private final static int INITIAL_DEGREE = 1000; + + private final static int MAX_OPTIMAL_DEGREE = 10000; + + private final List numberOfRetreat; + + public BackOffSpinLock() { + this.optimalDegree = INITIAL_DEGREE; + + numberOfRetreat = new ArrayList<>(2); + numberOfRetreat.add(new AtomicInteger(0)); + numberOfRetreat.add(new AtomicInteger(0)); + } + + @Override + public void lock() { + int spinDegree = this.optimalDegree; + while (true) { + for (int i = 0; i < spinDegree; i++) { + if (this.putMessageSpinLock.compareAndSet(true, false)) { + return; + } + } + numberOfRetreat.get(LocalTime.now().getSecond() % 2).getAndIncrement(); + try { + Thread.sleep(0); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + @Override + public void unlock() { + this.putMessageSpinLock.compareAndSet(false, true); + } + + @Override + public void update(MessageStoreConfig messageStoreConfig) { + this.optimalDegree = messageStoreConfig.getSpinLockCollisionRetreatOptimalDegree(); + } + + public int getOptimalDegree() { + return this.optimalDegree; + } + + public void setOptimalDegree(int optimalDegree) { + this.optimalDegree = optimalDegree; + } + + public boolean isAdapt() { + return optimalDegree < MAX_OPTIMAL_DEGREE; + } + + public synchronized void adapt(boolean isRise) { + if (isRise) { + if (optimalDegree * 2 <= MAX_OPTIMAL_DEGREE) { + optimalDegree *= 2; + } else { + if (optimalDegree + INITIAL_DEGREE <= MAX_OPTIMAL_DEGREE) { + optimalDegree += INITIAL_DEGREE; + } + } + } else { + if (optimalDegree >= 2 * INITIAL_DEGREE) { + optimalDegree -= INITIAL_DEGREE; + } + } + } + + public int getNumberOfRetreat(int pos) { + return numberOfRetreat.get(pos).get(); + } + + public void setNumberOfRetreat(int pos, int size) { + this.numberOfRetreat.get(pos).set(size); + } +} 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 0000000..28d443c --- /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 0000000..c490d09 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/logfile/DefaultMappedFile.java @@ -0,0 +1,946 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; + } + + @Override + public boolean appendMessageUsingFileChannel(byte[] data) { + int currentPos = WROTE_POSITION_UPDATER.get(this); + + if ((currentPos + data.length) <= this.fileSize) { + try { + this.fileChannel.position(currentPos); + this.fileChannel.write(ByteBuffer.wrap(data, 0, data.length)); + } catch (Throwable e) { + log.error("Error occurred when append message to mappedFile.", e); + } + WROTE_POSITION_UPDATER.addAndGet(this, data.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 0000000..fd70d6c --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/logfile/MappedFile.java @@ -0,0 +1,382 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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}. + * Using mappedByteBuffer + * + * @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}. + * Using fileChannel + * + * @param data the byte array to append + * @return true if success; false otherwise. + */ + boolean appendMessageUsingFileChannel(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 0000000..956501c --- /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 0000000..db4c7bb --- /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 0000000..6029488 --- /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 0000000..d5d6236 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/plugin/AbstractPluginMessageStore.java @@ -0,0 +1,669 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.exception.ConsumeQueueException; +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; + 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) throws ConsumeQueueException { + return next.getMaxOffsetInQueue(topic, queueId); + } + + @Override + public long getMaxOffsetInQueue(String topic, int queueId, boolean committed) throws ConsumeQueueException { + 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 dispatchBehindMilliseconds() { + return next.dispatchBehindMilliseconds(); + } + + @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 recoverTopicQueueTable() { + next.recoverTopicQueueTable(); + } + + @Override + public void notifyMessageArriveIfNecessary(DispatchRequest dispatchRequest) { + next.notifyMessageArriveIfNecessary(dispatchRequest); + } + + public MessageStore getNext() { + return next; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/plugin/MessageStoreFactory.java b/store/src/main/java/org/apache/rocketmq/store/plugin/MessageStoreFactory.java new file mode 100644 index 0000000..8d929ea --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/plugin/MessageStoreFactory.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.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/store/src/main/java/org/apache/rocketmq/store/plugin/MessageStorePluginContext.java b/store/src/main/java/org/apache/rocketmq/store/plugin/MessageStorePluginContext.java new file mode 100644 index 0000000..d39ccdd --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/plugin/MessageStorePluginContext.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.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 0000000..3e65c10 --- /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 0000000..991a1f0 --- /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 0000000..38e0a20 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.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.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; + @JSONField(name = "rp") + String rePutTimes; // ck rePut times + + 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 String getRePutTimes() { + return rePutTimes; + } + + public void setRePutTimes(String rePutTimes) { + this.rePutTimes = rePutTimes; + } + + 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); + } + + public int parseRePutTimes() { + if (null == rePutTimes) { + return 0; + } + try { + return Integer.parseInt(rePutTimes); + } catch (Exception e) { + } + return Byte.MAX_VALUE; + } + + @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 + ", rePutTimes=" + rePutTimes + "]"; + } + + @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 0000000..ef693dc --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/AbstractConsumeQueueStore.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.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.apache.rocketmq.store.exception.ConsumeQueueException; +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(); + if (messageStoreConfig.isEnableLmq()) { + this.consumeQueueTable = new ConcurrentHashMap<>(32_768); + } else { + this.consumeQueueTable = new ConcurrentHashMap<>(32); + } + } + + @Override + public void putMessagePositionInfoWrapper(ConsumeQueueInterface consumeQueue, DispatchRequest request) { + consumeQueue.putMessagePositionInfoWrapper(request); + } + + @Override + public Long getMaxOffset(String topic, int queueId) throws ConsumeQueueException { + 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 topic, int queueId, short delta) throws ConsumeQueueException { + queueOffsetOperator.increaseLmqOffset(topic, queueId, delta); + } + + @Override + public long getLmqQueueOffset(String topic, int queueId) throws ConsumeQueueException { + return queueOffsetOperator.getLmqOffset(topic, queueId, (t, q) -> 0L); + } + + @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(); + return this.messageStore.getCommitLog().pickupStoreTimestamp(phyOffset, size); + } catch (Exception e) { + log.error("Failed to getStoreTime", 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 0000000..1617182 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/BatchConsumeQueue.java @@ -0,0 +1,1178 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; + if (messageStore.getMessageStoreConfig().isPutConsumeQueueDataByFileChannel()) { + appendRes = mappedFile.appendMessageUsingFileChannel(this.byteBufferItem.array()); + } else { + 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 0000000..8ca85c6 --- /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 0000000..768c782 --- /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 0000000..5f9c2f9 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/ConsumeQueueStore.java @@ -0,0 +1,593 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.rocketmq.store.exception.StoreException; + +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() { + try { + flush(); + } catch (StoreException e) { + log.error("Failed to flush all consume queues", e); + return false; + } + 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 flush() throws StoreException { + for (Map.Entry> topicEntry : this.consumeQueueTable.entrySet()) { + for (Map.Entry cqEntry : topicEntry.getValue().entrySet()) { + flush(cqEntry.getValue(), 0); + } + } + } + + @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 0000000..72a481b --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/ConsumeQueueStoreInterface.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.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.apache.rocketmq.store.exception.ConsumeQueueException; +import org.apache.rocketmq.store.exception.StoreException; +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); + + /** + * Flush all nested consume queues to disk + * + * @throws StoreException if there is an error during flush + */ + void flush() throws StoreException; + + /** + * clean expired data from minCommitLogOffset + * @param minCommitLogOffset Minimum commit log offset + */ + void cleanExpired(long minCommitLogOffset); + + /** + * Check files. + */ + void checkSelf(); + + /** + * Delete expired files ending at min commit log position. + * @param consumeQueue + * @param minCommitLogOffset min commit log position + * @return deleted file numbers. + */ + int deleteExpiredFile(ConsumeQueueInterface consumeQueue, long minCommitLogOffset); + + /** + * 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 topic Topic/Queue name + * @param queueId Queue ID + * @param delta amount to increase + */ + void increaseLmqOffset(String topic, int queueId, short delta) throws ConsumeQueueException; + + /** + * get lmq queue offset + * @param topic + * @param queueId + * @return + */ + long getLmqQueueOffset(String topic, int queueId) throws ConsumeQueueException; + + /** + * 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 Topic name + * @param queueId Queue identifier + * @return the max offset in QueueOffsetOperator + * @throws ConsumeQueueException if there is an error while retrieving max consume queue offset + */ + Long getMaxOffset(String topic, int queueId) throws ConsumeQueueException; + + /** + * 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 0000000..34f5cb1 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/CqUnit.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.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 + + ", tagsCode=" + tagsCode + + ", compactedOffset=" + compactedOffset + + '}'; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/queue/DispatchEntry.java b/store/src/main/java/org/apache/rocketmq/store/queue/DispatchEntry.java new file mode 100644 index 0000000..a93ec0c --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/DispatchEntry.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.queue; + +import java.nio.charset.StandardCharsets; +import javax.annotation.Nonnull; +import org.apache.rocketmq.store.DispatchRequest; + +/** + * Use Record when Java 16 is available + */ +public class DispatchEntry { + public byte[] topic; + public int queueId; + public long queueOffset; + public long commitLogOffset; + public int messageSize; + public long tagCode; + public long storeTimestamp; + + public static DispatchEntry from(@Nonnull DispatchRequest request) { + DispatchEntry entry = new DispatchEntry(); + entry.topic = request.getTopic().getBytes(StandardCharsets.UTF_8); + entry.queueId = request.getQueueId(); + entry.queueOffset = request.getConsumeQueueOffset(); + entry.commitLogOffset = request.getCommitLogOffset(); + entry.messageSize = request.getMsgSize(); + entry.tagCode = request.getTagsCode(); + entry.storeTimestamp = request.getStoreTimestamp(); + return entry; + } +} 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 0000000..95cc088 --- /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 0000000..44397a2 --- /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/OffsetInitializer.java b/store/src/main/java/org/apache/rocketmq/store/queue/OffsetInitializer.java new file mode 100644 index 0000000..8d3a8cd --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/OffsetInitializer.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.queue; + +import org.apache.rocketmq.store.exception.ConsumeQueueException; + +public interface OffsetInitializer { + long maxConsumeQueueOffset(String topic, int queueId) throws ConsumeQueueException; +} diff --git a/store/src/main/java/org/apache/rocketmq/store/queue/OffsetInitializerRocksDBImpl.java b/store/src/main/java/org/apache/rocketmq/store/queue/OffsetInitializerRocksDBImpl.java new file mode 100644 index 0000000..4b889e1 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/OffsetInitializerRocksDBImpl.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.queue; + +import org.apache.rocketmq.store.exception.ConsumeQueueException; +import org.rocksdb.RocksDBException; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class OffsetInitializerRocksDBImpl implements OffsetInitializer { + + private static final Logger LOGGER = LoggerFactory.getLogger(OffsetInitializerRocksDBImpl.class); + + private final RocksDBConsumeQueueStore consumeQueueStore; + + public OffsetInitializerRocksDBImpl(RocksDBConsumeQueueStore consumeQueueStore) { + this.consumeQueueStore = consumeQueueStore; + } + + @Override + public long maxConsumeQueueOffset(String topic, int queueId) throws ConsumeQueueException { + try { + long offset = consumeQueueStore.getMaxOffsetInQueue(topic, queueId); + LOGGER.info("Look up RocksDB for max-offset of LMQ[{}:{}]: {}", topic, queueId, offset); + return offset; + } catch (RocksDBException e) { + throw new ConsumeQueueException(e); + } + } +} 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 0000000..7d38817 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/QueueOffsetOperator.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.queue; + +import com.google.common.base.Preconditions; +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; +import org.apache.rocketmq.store.exception.ConsumeQueueException; + +/** + * 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); + + /** + * {TOPIC}-{QUEUE_ID} --> NEXT Consume Queue Offset + */ + 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 topic, int queueId, OffsetInitializer callback) throws ConsumeQueueException { + Preconditions.checkNotNull(callback, "ConsumeQueueOffsetCallback cannot be null"); + String topicQueue = topic + "-" + queueId; + if (!lmqTopicQueueTable.containsKey(topicQueue)) { + // Load from RocksDB on cache miss. + Long prev = lmqTopicQueueTable.putIfAbsent(topicQueue, callback.maxConsumeQueueOffset(topic, queueId)); + if (null != prev) { + log.error("[BUG] Data racing, lmqTopicQueueTable should NOT contain key={}", topicQueue); + } + } + return lmqTopicQueueTable.get(topicQueue); + } + + public void increaseLmqOffset(String topic, int queueId, short delta) throws ConsumeQueueException { + String topicQueue = topic + "-" + queueId; + if (!this.lmqTopicQueueTable.containsKey(topicQueue)) { + throw new ConsumeQueueException(String.format("Max offset of Queue[name=%s, id=%d] should have existed", topic, queueId)); + } + long prev = lmqTopicQueueTable.get(topicQueue); + this.lmqTopicQueueTable.compute(topicQueue, (k, offset) -> offset + delta); + long current = lmqTopicQueueTable.get(topicQueue); + log.debug("Max offset of LMQ[{}:{}] increased: {} --> {}", topic, queueId, prev, current); + } + + 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; + } +} 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 0000000..eba8738 --- /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 0000000..7bd3c2e --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueue.java @@ -0,0 +1,483 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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) { + // Check from and to offset validity + Pair fromUnit = getCqUnitAndStoreTime(from); + if (fromUnit == null) { + return -1; + } + + if (from >= to) { + return -1; + } + + if (to > getMaxOffsetInQueue()) { + to = getMaxOffsetInQueue(); + } + + int maxSampleSize = messageStore.getMessageStoreConfig().getMaxConsumeQueueScan(); + int sampleSize = to - from > maxSampleSize ? maxSampleSize : (int) (to - from); + + int matchThreshold = messageStore.getMessageStoreConfig().getSampleCountThreshold(); + int matchSize = 0; + + for (int i = 0; i < sampleSize; i++) { + long index = from + i; + Pair pair = getCqUnitAndStoreTime(index); + if (pair == null) { + continue; + } + CqUnit cqUnit = pair.getObject1(); + if (filter.isMatchedByConsumeQueue(cqUnit.getTagsCode(), cqUnit.getCqExtUnit())) { + matchSize++; + // if matchSize is plenty, early exit estimate + if (matchSize > matchThreshold) { + sampleSize = i; + break; + } + } + } + // Make sure the second half is a floating point number, otherwise it will be truncated to 0 + return sampleSize == 0 ? 0 : (long) ((to - from) * (matchSize / (sampleSize * 1.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) { + return (int) diffLong; + } + return Integer.MAX_VALUE; + } + + @Override + public ReferredIterator iterateFrom(final long startIndex) { + long maxCqOffset = getMaxOffsetInQueue(); + if (startIndex < maxCqOffset) { + int num = pullNum(startIndex, maxCqOffset); + return new LargeRocksDBConsumeQueueIterator(startIndex, num); + } + 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 > 0 ? maxOffset - 1 : 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(); + } + } + } + + private class LargeRocksDBConsumeQueueIterator implements ReferredIterator { + private final long startIndex; + private final int totalCount; + private int currentIndex; + + public LargeRocksDBConsumeQueueIterator(final long startIndex, final int num) { + this.startIndex = startIndex; + this.totalCount = num; + this.currentIndex = 0; + } + + @Override + public boolean hasNext() { + return this.currentIndex < this.totalCount; + } + + + @Override + public CqUnit next() { + if (!hasNext()) { + return null; + } + + final ByteBuffer byteBuffer; + try { + byteBuffer = messageStore.getQueueStore().get(topic, queueId, startIndex + currentIndex); + } catch (RocksDBException e) { + ERROR_LOG.error("get cq from rocksdb failed. topic: {}, queueId: {}", topic, queueId, e); + return null; + } + if (byteBuffer == null || byteBuffer.remaining() < RocksDBConsumeQueueTable.CQ_UNIT_SIZE) { + return null; + } + 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 0000000..cb98985 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueueOffsetTable.java @@ -0,0 +1,743 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 io.netty.util.internal.PlatformDependent; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +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 java.util.function.Consumer; +import java.util.function.Function; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.Pair; +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.queue.offset.OffsetEntry; +import org.apache.rocketmq.store.queue.offset.OffsetEntryType; +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.config.AbstractRocksDBStorage.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(StandardCharsets.UTF_8); + private static final byte[] MIN_BYTES = "min".getBytes(StandardCharsets.UTF_8); + + /** + * 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 + */ + static final int OFFSET_PHY_OFFSET = 0; + 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) │ + * ├─────────────────────────┴───────────┴───────────┴───────────┴───────────┴─────────────┤ + */ + public 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(StandardCharsets.UTF_8); + 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 ConcurrentMap topicQueueMinOffset; + private final ConcurrentMap 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(); + loadMaxConsumeQueueOffsets(); + } + + private void loadMaxConsumeQueueOffsets() { + Function predicate = entry -> entry.type == OffsetEntryType.MAXIMUM; + Consumer fn = entry -> { + topicQueueMaxCqOffset.putIfAbsent(entry.topic + "-" + entry.queueId, entry.offset); + log.info("LoadMaxConsumeQueueOffsets Max {}:{} --> {}|{}", entry.topic, entry.queueId, entry.offset, entry.commitLogOffset); + }; + try { + forEach(predicate, fn); + } catch (RocksDBException e) { + log.error("Failed to maximum consume queue offset", e); + } + } + + public void forEach(Function predicate, Consumer fn) throws RocksDBException { + try (RocksIterator iterator = this.rocksDBStorage.seekOffsetCF()) { + if (null == iterator) { + return; + } + + int keyBufferCapacity = 256; + iterator.seekToFirst(); + ByteBuffer keyBuffer = ByteBuffer.allocateDirect(keyBufferCapacity); + ByteBuffer valueBuffer = ByteBuffer.allocateDirect(16); + while (iterator.isValid()) { + // parse key buffer according to key layout + keyBuffer.clear(); // clear position and limit before reuse + int total = iterator.key(keyBuffer); + if (total > keyBufferCapacity) { + keyBufferCapacity = total; + PlatformDependent.freeDirectBuffer(keyBuffer); + keyBuffer = ByteBuffer.allocateDirect(keyBufferCapacity); + continue; + } + + if (keyBuffer.remaining() <= OFFSET_KEY_LENGTH_WITHOUT_TOPIC_BYTES) { + iterator.next(); + ROCKSDB_LOG.warn("Malformed Key/Value pair"); + continue; + } + + int topicLength = keyBuffer.getInt(); + byte ctrl1 = keyBuffer.get(); + assert ctrl1 == CTRL_1; + + byte[] topicBytes = new byte[topicLength]; + keyBuffer.get(topicBytes); + ctrl1 = keyBuffer.get(); + assert ctrl1 == CTRL_1; + String topic = new String(topicBytes, StandardCharsets.UTF_8); + + byte[] minMax = new byte[3]; + keyBuffer.get(minMax); + OffsetEntryType entryType; + if (Arrays.equals(minMax, MAX_BYTES)) { + entryType = OffsetEntryType.MAXIMUM; + } else { + entryType = OffsetEntryType.MINIMUM; + } + ctrl1 = keyBuffer.get(); + assert ctrl1 == CTRL_1; + + assert keyBuffer.remaining() == Integer.BYTES; + int queueId = keyBuffer.getInt(); + + // Read and parse value buffer according to value layout + valueBuffer.clear(); // clear position and limit before reuse + total = iterator.value(valueBuffer); + if (total != Long.BYTES + Long.BYTES) { + // Skip system checkpoint topic as its value is only 8 bytes + iterator.next(); + continue; + } + long commitLogOffset = valueBuffer.getLong(); + long consumeOffset = valueBuffer.getLong(); + + OffsetEntry entry = new OffsetEntry(); + entry.topic = topic; + entry.queueId = queueId; + entry.type = entryType; + entry.offset = consumeOffset; + entry.commitLogOffset = commitLogOffset; + if (predicate.apply(entry)) { + fn.accept(entry); + } + iterator.next(); + } + // clean up direct buffers + PlatformDependent.freeDirectBuffer(keyBuffer); + PlatformDependent.freeDirectBuffer(valueBuffer); + } + } + + 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()) { + DispatchEntry dispatchEntry = entry.getValue().getObject2(); + String topic = new String(dispatchEntry.topic, StandardCharsets.UTF_8); + putHeapMaxCqOffset(topic, dispatchEntry.queueId, dispatchEntry.queueOffset); + } + } + + /** + * 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(StandardCharsets.UTF_8); + 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<>(); + + try (RocksIterator 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, StandardCharsets.UTF_8); + if (TopicValidator.isSystemTopic(topic)) { + continue; + } + + // LMQ topic offsets should NOT be removed + if (MixAll.isLmq(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); + } + 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); + long offset = maxCqOffset != null ? maxCqOffset : -1L; + Long prev = this.topicQueueMaxCqOffset.putIfAbsent(topicQueueId, offset); + if (null == prev) { + ROCKSDB_LOG.info("Max offset of {} is initialized to {} according to RocksDB", topicQueueId, offset); + } + if (messageStore.getMessageStoreConfig().isEnableRocksDBLog()) { + ROCKSDB_LOG.warn("updateMaxOffsetInQueue. {}, {}", topicQueueId, offset); + } + } + + return maxCqOffset; + } + + /** + * truncate dirty offset in rocksdb + * + * @param offsetToTruncate + * @throws RocksDBException + */ + public void truncateDirty(long offsetToTruncate) throws RocksDBException { + correctMaxPyhOffset(offsetToTruncate); + + Function predicate = entry -> { + if (entry.type == OffsetEntryType.MINIMUM) { + return false; + } + // Normal entry offset MUST have the following inequality + // entry commit-log offset + message-size-in-bytes <= offsetToTruncate; + // otherwise, the consume queue contains dirty records to truncate; + // + // If the broker node is configured to use async-flush, it's possible consume queues contain + // pointers to message records that is not flushed and lost during restart. + return entry.commitLogOffset >= offsetToTruncate; + }; + + Consumer fn = entry -> { + try { + truncateDirtyOffset(entry.topic, entry.queueId); + } catch (RocksDBException e) { + log.error("Failed to truncate maximum offset of consume queue[topic={}, queue-id={}]", + entry.topic, entry.queueId, e); + } + }; + + forEach(predicate, fn); + } + + 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(StandardCharsets.UTF_8); + 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 maxOffset) { + String topicQueueId = buildTopicQueueId(topic, queueId); + Long prev = this.topicQueueMaxCqOffset.put(topicQueueId, maxOffset); + if (prev != null && prev > maxOffset) { + ERROR_LOG.error("Max offset of consume-queue[topic={}, queue-id={}] regressed. prev-max={}, current-max={}", + topic, queueId, prev, maxOffset); + } + } + + 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; + } + try (WriteBatch writeBatch = new WriteBatch()) { + final byte[] topicBytes = topic.getBytes(StandardCharsets.UTF_8); + 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 { + 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"); + } + + PhyAndCQOffset targetPhyAndCQOffset = this.rocksDBConsumeQueueTable.binarySearchInCQ(topic, queueId, maxCQOffset, + minCQOffset, 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; + } + + PhyAndCQOffset phyAndCQOffset = this.rocksDBConsumeQueueTable.binarySearchInCQ(topic, queueId, maxCQOffset, + minCQOffset, 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); + } + + static void buildOffsetKeyAndValueByteBuffer(final Pair offsetBBPair, + final DispatchEntry entry) { + final ByteBuffer offsetKey = offsetBBPair.getObject1(); + buildOffsetKeyByteBuffer(offsetKey, entry.topic, entry.queueId, true); + + final ByteBuffer offsetValue = offsetBBPair.getObject2(); + buildOffsetValueByteBuffer(offsetValue, entry.commitLogOffset, entry.queueOffset); + } + + private static 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; + } + + public static 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 static void buildOffsetValueByteBuffer(final ByteBuffer byteBuffer, final long phyOffset, + final long cqOffset) { + byteBuffer.position(0).limit(OFFSET_VALUE_LENGTH); + buildOffsetValueByteBuffer0(byteBuffer, phyOffset, cqOffset); + } + + private static ByteBuffer buildOffsetValueByteBuffer(final long phyOffset, final long cqOffset) { + final ByteBuffer byteBuffer = ByteBuffer.allocate(OFFSET_VALUE_LENGTH); + buildOffsetValueByteBuffer0(byteBuffer, phyOffset, cqOffset); + return byteBuffer; + } + + private static 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 0000000..94ed0c9 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueueStore.java @@ -0,0 +1,565 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.nio.charset.StandardCharsets; +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 java.util.concurrent.atomic.AtomicReference; +import javax.annotation.Nonnull; +import org.apache.commons.io.FileUtils; +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.ServiceState; +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.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.StorePathConfigHelper; +import org.apache.rocketmq.store.exception.ConsumeQueueException; +import org.apache.rocketmq.store.exception.StoreException; +import org.apache.rocketmq.store.rocksdb.ConsumeQueueRocksDBStorage; +import org.rocksdb.FlushOptions; +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); + + private static final int DEFAULT_BYTE_BUFFER_CAPACITY = 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 List> cqBBPairList; + private final List> offsetBBPairList; + private final Map> tempTopicQueueMaxOffsetMap; + private volatile boolean isCQError = false; + + private int consumeQueueByteBufferCacheIndex; + private int offsetBufferCacheIndex; + + private final OffsetInitializer offsetInitializer; + + private final RocksGroupCommitService groupCommitService; + + private final AtomicReference serviceState = new AtomicReference<>(ServiceState.CREATE_JUST); + + public RocksDBConsumeQueueStore(DefaultMessageStore messageStore) { + super(messageStore); + + this.storePath = StorePathConfigHelper.getStorePathConsumeQueue(messageStoreConfig.getStorePathRootDir()); + this.rocksDBStorage = new ConsumeQueueRocksDBStorage(messageStore, storePath); + this.rocksDBConsumeQueueTable = new RocksDBConsumeQueueTable(rocksDBStorage, messageStore); + this.rocksDBConsumeQueueOffsetTable = new RocksDBConsumeQueueOffsetTable(rocksDBConsumeQueueTable, rocksDBStorage, messageStore); + + this.offsetInitializer = new OffsetInitializerRocksDBImpl(this); + this.groupCommitService = new RocksGroupCommitService(this); + this.cqBBPairList = new ArrayList<>(16); + this.offsetBBPairList = new ArrayList<>(DEFAULT_BYTE_BUFFER_CAPACITY); + for (int i = 0; i < DEFAULT_BYTE_BUFFER_CAPACITY; i++) { + this.cqBBPairList.add(RocksDBConsumeQueueTable.getCQByteBufferPair()); + this.offsetBBPairList.add(RocksDBConsumeQueueOffsetTable.getOffsetByteBufferPair()); + } + + this.tempTopicQueueMaxOffsetMap = new HashMap<>(); + this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor( + new ThreadFactoryImpl("RocksDBConsumeQueueStoreScheduledThread", messageStore.getBrokerIdentity())); + } + + private Pair getCQByteBufferPair() { + int idx = consumeQueueByteBufferCacheIndex++; + if (idx >= cqBBPairList.size()) { + this.cqBBPairList.add(RocksDBConsumeQueueTable.getCQByteBufferPair()); + } + return cqBBPairList.get(idx); + } + + private Pair getOffsetByteBufferPair() { + int idx = offsetBufferCacheIndex++; + if (idx >= offsetBBPairList.size()) { + this.offsetBBPairList.add(RocksDBConsumeQueueOffsetTable.getOffsetByteBufferPair()); + } + return offsetBBPairList.get(idx); + } + + @Override + public void start() { + if (serviceState.compareAndSet(ServiceState.CREATE_JUST, ServiceState.RUNNING)) { + log.info("RocksDB ConsumeQueueStore start!"); + this.groupCommitService.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() { + start(); + } + + @Override + public boolean recoverConcurrently() { + start(); + return true; + } + + @Override + public boolean shutdown() { + if (serviceState.compareAndSet(ServiceState.RUNNING, ServiceState.SHUTDOWN_ALREADY)) { + this.groupCommitService.shutdown(); + this.scheduledExecutorService.shutdown(); + return shutdownInner(); + } + return true; + } + + private boolean shutdownInner() { + return this.rocksDBStorage.shutdown(); + } + + @Override + public void putMessagePositionInfoWrapper(DispatchRequest request) throws RocksDBException { + if (null == request) { + return; + } + + try { + groupCommitService.putRequest(request); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + public void putMessagePosition(List requests) throws RocksDBException { + final int maxRetries = 30; + for (int i = 0; i < maxRetries; i++) { + if (putMessagePosition0(requests)) { + 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(List requests) { + if (!this.rocksDBStorage.hold()) { + return false; + } + + try (WriteBatch writeBatch = new WriteBatch()) { + final int size = requests.size(); + if (size == 0) { + return true; + } + long maxPhyOffset = 0; + for (int i = size - 1; i >= 0; i--) { + final DispatchRequest request = requests.get(i); + DispatchEntry entry = DispatchEntry.from(request); + dispatch(entry, writeBatch); + dispatchLMQ(request, writeBatch); + + final int msgSize = request.getMsgSize(); + final long phyOffset = request.getCommitLogOffset(); + if (phyOffset + msgSize >= maxPhyOffset) { + maxPhyOffset = phyOffset + msgSize; + } + } + + this.rocksDBConsumeQueueOffsetTable.putMaxPhyAndCqOffset(tempTopicQueueMaxOffsetMap, writeBatch, maxPhyOffset); + + this.rocksDBStorage.batchPut(writeBatch); + + this.rocksDBConsumeQueueOffsetTable.putHeapMaxCqOffset(tempTopicQueueMaxOffsetMap); + notifyMessageArriveAndClear(requests); + return true; + } catch (Exception e) { + ERROR_LOG.error("putMessagePosition0 failed.", e); + return false; + } finally { + tempTopicQueueMaxOffsetMap.clear(); + consumeQueueByteBufferCacheIndex = 0; + offsetBufferCacheIndex = 0; + this.rocksDBStorage.release(); + } + } + + private void dispatch(@Nonnull DispatchEntry entry, @Nonnull final WriteBatch writeBatch) throws RocksDBException { + this.rocksDBConsumeQueueTable.buildAndPutCQByteBuffer(getCQByteBufferPair(), entry, writeBatch); + updateTempTopicQueueMaxOffset(getOffsetByteBufferPair(), entry); + } + + private void updateTempTopicQueueMaxOffset(final Pair offsetBBPair, + final DispatchEntry entry) { + RocksDBConsumeQueueOffsetTable.buildOffsetKeyAndValueByteBuffer(offsetBBPair, entry); + ByteBuffer topicQueueId = offsetBBPair.getObject1(); + ByteBuffer maxOffsetBB = offsetBBPair.getObject2(); + Pair old = tempTopicQueueMaxOffsetMap.get(topicQueueId); + if (old == null) { + tempTopicQueueMaxOffsetMap.put(topicQueueId, new Pair<>(maxOffsetBB, entry)); + } else { + long oldMaxOffset = old.getObject1().getLong(RocksDBConsumeQueueOffsetTable.OFFSET_CQ_OFFSET); + long maxOffset = maxOffsetBB.getLong(RocksDBConsumeQueueOffsetTable.OFFSET_CQ_OFFSET); + if (maxOffset >= oldMaxOffset) { + ERROR_LOG.error("cqOffset invalid1. old: {}, now: {}", oldMaxOffset, maxOffset); + } + } + } + + private void dispatchLMQ(@Nonnull DispatchRequest request, @Nonnull final WriteBatch writeBatch) + throws RocksDBException { + if (!messageStoreConfig.isEnableLmq() || !request.containsLMQ()) { + return; + } + Map map = request.getPropertiesMap(); + String lmqNames = map.get(MessageConst.PROPERTY_INNER_MULTI_DISPATCH); + String lmqOffsets = map.get(MessageConst.PROPERTY_INNER_MULTI_QUEUE_OFFSET); + String[] queues = lmqNames.split(MixAll.LMQ_DISPATCH_SEPARATOR); + String[] queueOffsets = lmqOffsets.split(MixAll.LMQ_DISPATCH_SEPARATOR); + if (queues.length != queueOffsets.length) { + ERROR_LOG.error("[bug] queues.length!=queueOffsets.length ", request.getTopic()); + return; + } + for (int i = 0; i < queues.length; i++) { + String queueName = queues[i]; + DispatchEntry entry = DispatchEntry.from(request); + long queueOffset = Long.parseLong(queueOffsets[i]); + int queueId = request.getQueueId(); + if (this.messageStore.getMessageStoreConfig().isEnableLmq() && MixAll.isLmq(queueName)) { + queueId = MixAll.LMQ_QUEUE_ID; + } + entry.queueId = queueId; + entry.queueOffset = queueOffset; + entry.topic = queueName.getBytes(StandardCharsets.UTF_8); + log.debug("Dispatch LMQ[{}:{}]:{} --> {}", queueName, queueId, queueOffset, entry.commitLogOffset); + dispatch(entry, writeBatch); + } + } + + private void notifyMessageArriveAndClear(List requests) { + try { + for (DispatchRequest dp : requests) { + this.messageStore.notifyMessageArriveIfNecessary(dp); + } + requests.clear(); + } catch (Exception e) { + ERROR_LOG.error("notifyMessageArriveAndClear Failed.", e); + } + } + + 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; + } + + try (WriteBatch writeBatch = new WriteBatch()) { + 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 { + this.rocksDBStorage.release(); + } + } + + @Override + public boolean flush(ConsumeQueueInterface consumeQueue, int flushLeastPages) { + try { + this.rocksDBStorage.flushWAL(); + } catch (Exception e) { + log.error("Failed to flush WAL", e); + } + return true; + } + + @Override + public void flush() throws StoreException { + try (FlushOptions flushOptions = new FlushOptions()) { + flushOptions.setWaitForFlush(true); + flushOptions.setAllowWriteStall(true); + this.rocksDBStorage.flush(flushOptions); + } catch (RocksDBException e) { + throw new StoreException(e); + } + } + + @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 CommitLog offset to truncate to + * @throws RocksDBException If there is any error. + */ + @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, boundaryType); + } + + /** + * This method actually returns NEXT slot index to use, starting from 0. For example, if the queue is empty, + * it returns 0, pointing to the first slot of the 0-based queue; + * + * @param topic Topic name + * @param queueId Queue ID + * @return Index of the next slot to push into + * @throws RocksDBException if RocksDB fails to fulfill the request. + */ + @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; + if (MixAll.isLmq(topic)) { + // For LMQ, no need to over allocate internal hashtable + newMap = new ConcurrentHashMap<>(1, 1.0F); + } else { + newMap = new ConcurrentHashMap<>(8); + } + 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; + } + + @Override + public long getLmqQueueOffset(String topic, int queueId) throws ConsumeQueueException { + return queueOffsetOperator.getLmqOffset(topic, queueId, offsetInitializer); + } + + @Override + public Long getMaxOffset(String topic, int queueId) throws ConsumeQueueException { + if (MixAll.isLmq(topic)) { + return getLmqQueueOffset(topic, queueId); + } + return super.getMaxOffset(topic, queueId); + } + + public boolean isStopped() { + return ServiceState.SHUTDOWN_ALREADY == serviceState.get(); + } +} 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 0000000..deee029 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueueTable.java @@ -0,0 +1,387 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import org.apache.rocketmq.common.BoundaryType; +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.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.config.AbstractRocksDBStorage.CTRL_0; +import static org.apache.rocketmq.common.config.AbstractRocksDBStorage.CTRL_1; +import static org.apache.rocketmq.common.config.AbstractRocksDBStorage.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 DispatchEntry request, + final WriteBatch writeBatch) throws RocksDBException { + final ByteBuffer cqKey = cqBBPair.getObject1(); + buildCQKeyByteBuffer(cqKey, request.topic, request.queueId, request.queueOffset); + + final ByteBuffer cqValue = cqBBPair.getObject2(); + buildCQValueByteBuffer(cqValue, request.commitLogOffset, request.messageSize, request.tagCode, request.storeTimestamp); + + 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(StandardCharsets.UTF_8); + 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(StandardCharsets.UTF_8); + 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(StandardCharsets.UTF_8); + 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, BoundaryType boundaryType) throws RocksDBException { + long result = -1L; + long targetOffset = -1L, leftOffset = -1L, rightOffset = -1L; + long ceiling = high, floor = low; + // Handle the following corner cases first: + // 1. store time of (high) < timestamp + ByteBuffer buffer = getCQInKV(topic, queueId, ceiling); + if (buffer != null) { + long storeTime = buffer.getLong(MSG_STORE_TIME_SIZE_OFFSET); + if (storeTime < timestamp) { + switch (boundaryType) { + case LOWER: + return ceiling + 1; + case UPPER: + return ceiling; + default: + log.warn("Unknown boundary type"); + break; + } + } + } + // 2. store time of (low) > timestamp + buffer = getCQInKV(topic, queueId, floor); + if (buffer != null) { + long storeTime = buffer.getLong(MSG_STORE_TIME_SIZE_OFFSET); + if (storeTime > timestamp) { + switch (boundaryType) { + case LOWER: + return floor; + case UPPER: + return 0; + default: + log.warn("Unknown boundary type"); + break; + } + } + } + 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; + } else { + low = midOffset + 1; + leftOffset = midOffset; + } + } + if (targetOffset != -1) { + // offset next to it might also share the same store-timestamp. + switch (boundaryType) { + case LOWER: { + while (true) { + long nextOffset = targetOffset - 1; + if (nextOffset < floor) { + break; + } + ByteBuffer byteBuffer = getCQInKV(topic, queueId, nextOffset); + long storeTime = byteBuffer.getLong(MSG_STORE_TIME_SIZE_OFFSET); + if (storeTime != timestamp) { + break; + } + targetOffset = nextOffset; + } + break; + } + case UPPER: { + while (true) { + long nextOffset = targetOffset + 1; + if (nextOffset > ceiling) { + break; + } + ByteBuffer byteBuffer = getCQInKV(topic, queueId, nextOffset); + long storeTime = byteBuffer.getLong(MSG_STORE_TIME_SIZE_OFFSET); + if (storeTime != timestamp) { + break; + } + targetOffset = nextOffset; + } + break; + } + default: { + log.warn("Unknown boundary type"); + break; + } + } + result = targetOffset; + } else { + switch (boundaryType) { + case LOWER: { + result = rightOffset; + break; + } + case UPPER: { + result = leftOffset; + break; + } + default: { + log.warn("Unknown boundary type"); + break; + } + } + } + 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/RocksGroupCommitService.java b/store/src/main/java/org/apache/rocketmq/store/queue/RocksGroupCommitService.java new file mode 100644 index 0000000..e2f2c9e --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/RocksGroupCommitService.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.store.queue; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.store.DispatchRequest; +import org.rocksdb.RocksDBException; + +public class RocksGroupCommitService extends ServiceThread { + + private static final int MAX_BUFFER_SIZE = 100_000; + + private static final int PREFERRED_DISPATCH_REQUEST_COUNT = 256; + + private final LinkedBlockingQueue buffer; + + private final RocksDBConsumeQueueStore store; + + private final List requests = new ArrayList<>(PREFERRED_DISPATCH_REQUEST_COUNT); + + public RocksGroupCommitService(RocksDBConsumeQueueStore store) { + this.store = store; + this.buffer = new LinkedBlockingQueue<>(MAX_BUFFER_SIZE); + } + + @Override + public String getServiceName() { + return "RocksGroupCommit"; + } + + @Override + public void run() { + log.info("{} service started", this.getServiceName()); + while (!this.isStopped()) { + try { + this.waitForRunning(10); + this.doCommit(); + } catch (Exception e) { + log.warn("{} service has exception. ", this.getServiceName(), e); + } + } + log.info("{} service end", this.getServiceName()); + } + + public void putRequest(final DispatchRequest request) throws InterruptedException { + while (!buffer.offer(request, 3, TimeUnit.SECONDS)) { + log.warn("RocksGroupCommitService#buffer is full, 3s elapsed before space becomes available"); + } + this.wakeup(); + } + + private void doCommit() { + while (!buffer.isEmpty()) { + while (true) { + DispatchRequest dispatchRequest = buffer.poll(); + if (null != dispatchRequest) { + requests.add(dispatchRequest); + } + + if (requests.isEmpty()) { + // buffer has been drained + break; + } + + if (null == dispatchRequest || requests.size() >= PREFERRED_DISPATCH_REQUEST_COUNT) { + groupCommit(); + } + } + } + } + + private void groupCommit() { + while (!store.isStopped()) { + try { + // putMessagePosition will clear requests after consume queue building completion + store.putMessagePosition(requests); + break; + } catch (RocksDBException e) { + log.error("Failed to build consume queue in RocksDB", e); + } + } + } + +} 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 0000000..7e14de3 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/SparseConsumeQueue.java @@ -0,0 +1,405 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; + + if (messageStore.getMessageStoreConfig().isPutConsumeQueueDataByFileChannel()) { + appendRes = mappedFile.appendMessageUsingFileChannel(this.byteBufferItem.array()); + } else { + 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/queue/offset/OffsetEntry.java b/store/src/main/java/org/apache/rocketmq/store/queue/offset/OffsetEntry.java new file mode 100644 index 0000000..bac3aa4 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/offset/OffsetEntry.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.store.queue.offset; + +public class OffsetEntry { + /** + * Topic identifier. For now, it's topic name directly. In the future, we should use fixed length topic identifier. + */ + public String topic; + + /** + * Queue ID + */ + public int queueId; + + /** + * Flag if the entry is for maximum or minimum + */ + public OffsetEntryType type; + + /** + * Maximum or minimum consume-queue offset. + */ + public long offset; + + /** + * Maximum or minimum commit-log offset. + */ + public long commitLogOffset; +} diff --git a/store/src/main/java/org/apache/rocketmq/store/queue/offset/OffsetEntryType.java b/store/src/main/java/org/apache/rocketmq/store/queue/offset/OffsetEntryType.java new file mode 100644 index 0000000..78df4e1 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/offset/OffsetEntryType.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.queue.offset; + +public enum OffsetEntryType { + MAXIMUM, + MINIMUM +} 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 0000000..aa796c4 --- /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 0000000..b343a5b --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/rocksdb/ConsumeQueueRocksDBStorage.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.store.rocksdb; + +import java.nio.charset.StandardCharsets; +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.store.MessageStore; +import org.rocksdb.ColumnFamilyDescriptor; +import org.rocksdb.ColumnFamilyHandle; +import org.rocksdb.ColumnFamilyOptions; +import org.rocksdb.ReadOptions; +import org.rocksdb.RocksDB; +import org.rocksdb.RocksDBException; +import org.rocksdb.RocksIterator; +import org.rocksdb.WriteBatch; + +public class ConsumeQueueRocksDBStorage extends AbstractRocksDBStorage { + + public static final byte[] OFFSET_COLUMN_FAMILY = "offset".getBytes(StandardCharsets.UTF_8); + + private final MessageStore messageStore; + private volatile ColumnFamilyHandle offsetCFHandle; + + public ConsumeQueueRocksDBStorage(final MessageStore messageStore, final String dbPath) { + super(dbPath); + this.messageStore = messageStore; + this.readOnly = false; + } + + protected void initOptions() { + this.options = RocksDBOptionsFactory.createDBOptions(); + super.initOptions(); + } + + @Override + protected void initTotalOrderReadOptions() { + this.totalOrderReadOptions = new ReadOptions(); + this.totalOrderReadOptions.setPrefixSameAsStart(false); + this.totalOrderReadOptions.setTotalOrderSeek(false); + } + + @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_COLUMN_FAMILY, offsetCfOptions)); + open(cfDescriptors); + 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; + } +} 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 0000000..5687d6a --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/rocksdb/RocksDBOptionsFactory.java @@ -0,0 +1,219 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.ConfigHelper; +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.CompactionPriority; +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); + String bottomMostCompressionTypeOpt = messageStore.getMessageStoreConfig() + .getBottomMostCompressionTypeForConsumeQueueStore(); + String compressionTypeOpt = messageStore.getMessageStoreConfig() + .getRocksdbCompressionType(); + CompressionType bottomMostCompressionType = CompressionType.getCompressionType(bottomMostCompressionTypeOpt); + CompressionType compressionType = CompressionType.getCompressionType(compressionTypeOpt); + return columnFamilyOptions.setMaxWriteBufferNumber(4). + setWriteBufferSize(128 * SizeUnit.MB). + setMinWriteBufferNumberToMerge(1). + setTableFormatConfig(blockBasedTableConfig). + setMemTableConfig(new SkipListMemTableConfig()). + setCompressionType(compressionType). + setBottommostCompressionType(bottomMostCompressionType). + setNumLevels(7). + setCompactionPriority(CompactionPriority.MinOverlappingRatio). + 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); + } + + public static ColumnFamilyOptions createPopCFOptions() { + 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); + + CompactionOptionsUniversal compactionOption = new CompactionOptionsUniversal() + .setSizeRatio(100) + .setMaxSizeAmplificationPercent(25) + .setAllowTrivialMove(true) + .setMinMergeWidth(2) + .setMaxMergeWidth(Integer.MAX_VALUE) + .setStopStyle(CompactionStopStyle.CompactionStopStyleTotalSize) + .setCompressionSizePercent(-1); + + //noinspection resource + return new ColumnFamilyOptions() + .setMaxWriteBufferNumber(4) + .setWriteBufferSize(128 * SizeUnit.MB) + .setMinWriteBufferNumberToMerge(1) + .setTableFormatConfig(blockBasedTableConfig) + .setMemTableConfig(new SkipListMemTableConfig()) + .setCompressionType(CompressionType.NO_COMPRESSION) + .setBottommostCompressionType(CompressionType.NO_COMPRESSION) + .setNumLevels(7) + .setCompactionPriority(CompactionPriority.MinOverlappingRatio) + .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()) + .setReportBgIoStats(true) + .setOptimizeFiltersForHits(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(ConfigHelper.getDBLogDir()). + setInfoLogLevel(InfoLogLevel.INFO_LEVEL). + setWalRecoveryMode(WALRecoveryMode.PointInTimeRecovery). + setManualWalFlush(true). + setCreateIfMissing(true). + setBytesPerSync(SizeUnit.MB). + setCreateMissingColumnFamilies(true). + setMaxOpenFiles(-1). + setMaxLogFileSize(SizeUnit.GB). + setKeepLogFileNum(5). + setMaxManifestFileSize(SizeUnit.GB). + setAllowConcurrentMemtableWrite(false). + setStatistics(statistics). + setAtomicFlush(true). + setCompactionReadaheadSize(4 * SizeUnit.MB). + 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/stats/BrokerStats.java b/store/src/main/java/org/apache/rocketmq/store/stats/BrokerStats.java new file mode 100644 index 0000000..fb71755 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/stats/BrokerStats.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.store.stats; + +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; + +public class BrokerStats { + private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + + private final MessageStore defaultMessageStore; + + private volatile long msgPutTotalYesterdayMorning; + + private volatile long msgPutTotalTodayMorning; + + private volatile long msgGetTotalYesterdayMorning; + + private volatile long msgGetTotalTodayMorning; + + public BrokerStats(MessageStore defaultMessageStore) { + this.defaultMessageStore = defaultMessageStore; + } + + public void record() { + this.msgPutTotalYesterdayMorning = this.msgPutTotalTodayMorning; + this.msgGetTotalYesterdayMorning = this.msgGetTotalTodayMorning; + + this.msgPutTotalTodayMorning = + this.defaultMessageStore.getBrokerStatsManager().getBrokerPutNumsWithoutSystemTopic(); + this.msgGetTotalTodayMorning = + this.defaultMessageStore.getBrokerStatsManager().getBrokerGetNumsWithoutSystemTopic(); + + log.info("yesterday put message total: {}", msgPutTotalTodayMorning - msgPutTotalYesterdayMorning); + log.info("yesterday get message total: {}", msgGetTotalTodayMorning - msgGetTotalYesterdayMorning); + } + + public long getMsgPutTotalYesterdayMorning() { + return msgPutTotalYesterdayMorning; + } + + public void setMsgPutTotalYesterdayMorning(long msgPutTotalYesterdayMorning) { + this.msgPutTotalYesterdayMorning = msgPutTotalYesterdayMorning; + } + + public long getMsgPutTotalTodayMorning() { + return msgPutTotalTodayMorning; + } + + public void setMsgPutTotalTodayMorning(long msgPutTotalTodayMorning) { + this.msgPutTotalTodayMorning = msgPutTotalTodayMorning; + } + + public long getMsgGetTotalYesterdayMorning() { + return msgGetTotalYesterdayMorning; + } + + public void setMsgGetTotalYesterdayMorning(long msgGetTotalYesterdayMorning) { + this.msgGetTotalYesterdayMorning = msgGetTotalYesterdayMorning; + } + + public long getMsgGetTotalTodayMorning() { + return msgGetTotalTodayMorning; + } + + public void setMsgGetTotalTodayMorning(long msgGetTotalTodayMorning) { + this.msgGetTotalTodayMorning = msgGetTotalTodayMorning; + } + + public long getMsgPutTotalTodayNow() { + return this.defaultMessageStore.getBrokerStatsManager().getBrokerPutNumsWithoutSystemTopic(); + } + + public long getMsgGetTotalTodayNow() { + 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 new file mode 100644 index 0000000..c272a30 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/stats/BrokerStatsManager.java @@ -0,0 +1,825 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 java.util.HashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +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.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 { + + @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 + @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 + @Deprecated public static final String TOPIC_PUT_LATENCY = "TOPIC_PUT_LATENCY"; + @Deprecated public static final String GROUP_ACK_NUMS = "GROUP_ACK_NUMS"; + @Deprecated 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"; + + 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 + @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"; + private static final String[] NEED_CLEAN_STATS_SET = + new String[] {TOPIC_PUT_NUMS, TOPIC_PUT_SIZE, GROUP_GET_NUMS, GROUP_GET_SIZE, SNDBCK_PUT_NUMS, GROUP_GET_LATENCY}; + + /** + * 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 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 ScheduledExecutorService cleanResourceExecutor; + + private final HashMap statsTable = new HashMap<>(); + private final String clusterName; + private final boolean enableQueueStat; + private MomentStatsItemSet momentStatsItemSetFallSize; + private MomentStatsItemSet momentStatsItemSetFallTime; + + private final StatisticsManager accountStatManager = new StatisticsManager(); + private StateGetter producerStateGetter; + private StateGetter consumerStateGetter; + + 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(Stats.GROUP_ACK_NUMS, new StatsItemSet(Stats.GROUP_ACK_NUMS, this.scheduledExecutorService, log)); + this.statsTable.put(Stats.GROUP_CK_NUMS, new StatsItemSet(Stats.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(Stats.TOPIC_PUT_LATENCY, new StatsItemSet(Stats.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(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 producerStateGetter.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; + } + }); + cleanResourceExecutor.scheduleWithFixedDelay(new Runnable() { + @Override + public void run() { + cleanAllResource(); + } + }, 10, 10, TimeUnit.MINUTES); + } + + 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)); + this.cleanResourceExecutor = + ThreadUtils.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("CleanStatsResourceThread", true, brokerConfig)); + } + + public MomentStatsItemSet getMomentStatsItemSetFallSize() { + return momentStatsItemSetFallSize; + } + + public MomentStatsItemSet getMomentStatsItemSetFallTime() { + return momentStatsItemSetFallTime; + } + + public StateGetter getProducerStateGetter() { + return producerStateGetter; + } + + public void setProducerStateGetter(StateGetter producerStateGetter) { + this.producerStateGetter = producerStateGetter; + } + + 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(); + this.cleanResourceExecutor.shutdown(); + } + + public StatsItem getStatsItem(final String statsName, final String statsKey) { + try { + return this.statsTable.get(statsName).getStatsItem(statsKey); + } catch (Exception e) { + } + + 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.GROUP_CK_NUMS).delValueByPrefixKey(topic, "@"); + this.statsTable.get(Stats.GROUP_ACK_NUMS).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.statsTable.get(Stats.TOPIC_PUT_LATENCY).delValueBySuffixKey(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, "@"); + this.statsTable.get(Stats.GROUP_CK_NUMS).delValueBySuffixKey(group, "@"); + this.statsTable.get(Stats.GROUP_ACK_NUMS).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(Stats.TOPIC_PUT_NUMS).addValue(topic, 1, 1); + } + + public void incTopicPutNums(final String topic, int num, int times) { + this.statsTable.get(Stats.TOPIC_PUT_NUMS).addValue(topic, num, times); + } + + public void incTopicPutSize(final String topic, final int size) { + 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(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(Stats.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(Stats.GROUP_ACK_NUMS).addValue(statsKey, incValue, 1); + } + + public String buildStatsKey(String topic, String 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(Stats.GROUP_GET_SIZE).addValue(statsKey, incValue, 1); + } + + public void incGroupGetLatency(final String group, final String topic, final int queueId, final int incValue) { + 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(Stats.TOPIC_PUT_LATENCY).addValue(statsKey.toString(), incValue, 1); + } + public void incBrokerPutNums() { + 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 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 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(Stats.SNDBCK_PUT_NUMS).addValue(statsKey, 1, 1); + } + + public double tpsTopicPutNums(final String topic) { + return this.statsTable.get(TOPIC_PUT_NUMS).getStatsDataInMinute(topic).getTps(); + } + + public double tpsGroupGetNums(final String group, final String topic) { + final String statsKey = buildStatsKey(topic, group); + 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 = buildStatsKey(queueId, topic, group); + this.momentStatsItemSetFallTime.setValue(statsKey, fallBehind); + } + + public void recordDiskFallBehindSize(final String group, final String topic, final int queueId, + final long fallBehind) { + final String statsKey = buildStatsKey(queueId, topic, group); + this.momentStatsItemSetFallSize.setValue(statsKey, 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) { + StringBuilder strBuilder = new StringBuilder(); + strBuilder.append(owner); + strBuilder.append("@"); + strBuilder.append(topic); + strBuilder.append("@"); + strBuilder.append(group); + strBuilder.append("@"); + strBuilder.append(type); + 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); + } + + + private void cleanAllResource() { + try { + int maxStatsIdleTimeInMinutes = brokerConfig != null ? brokerConfig.getMaxStatsIdleTimeInMinutes() : -1; + if (maxStatsIdleTimeInMinutes < 0) { + COMMERCIAL_LOG.info("[BrokerStatsManager#cleanAllResource] maxStatsIdleTimeInMinutes={}, no need to clean resource", maxStatsIdleTimeInMinutes); + return; + } + if (maxStatsIdleTimeInMinutes <= 10 && maxStatsIdleTimeInMinutes >= 0) { + maxStatsIdleTimeInMinutes = 30; + } + for (String statsKind : NEED_CLEAN_STATS_SET) { + StatsItemSet statsItemSet = this.statsTable.get(statsKind); + if (null == statsItemSet) { + continue; + } + statsItemSet.cleanResource(maxStatsIdleTimeInMinutes); + } + momentStatsItemSetFallSize.cleanResource(maxStatsIdleTimeInMinutes); + momentStatsItemSetFallTime.cleanResource(maxStatsIdleTimeInMinutes); + } catch (Throwable throwable) { + COMMERCIAL_LOG.error("[BrokerStatsManager#cleanAllResource] clean resource error", throwable); + } + } + + public enum StatsType { + SEND_SUCCESS, + SEND_FAILURE, + + RCV_SUCCESS, + RCV_EPOLLS, + SEND_BACK, + SEND_BACK_TO_DLQ, + + SEND_ORDER, + SEND_TIMER, + SEND_TRANSACTION, + + 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 0000000..4caea19 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/stats/LmqBrokerStatsManager.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.store.stats; + +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; + +public class LmqBrokerStatsManager extends BrokerStatsManager { + + private final BrokerConfig brokerConfig; + + public LmqBrokerStatsManager(BrokerConfig brokerConfig) { + super(brokerConfig.getBrokerClusterName(), brokerConfig.isEnableDetailStat()); + this.brokerConfig = brokerConfig; + } + + @Override + public void incGroupGetNums(final String group, final String topic, final int incValue) { + super.incGroupGetNums(getAdjustedGroup(group), getAdjustedTopic(topic), incValue); + } + + @Override + public void incGroupGetSize(final String group, final String topic, final int incValue) { + super.incGroupGetSize(getAdjustedGroup(group), getAdjustedTopic(topic), incValue); + } + + @Override + public void incGroupAckNums(final String group, final String topic, final int incValue) { + super.incGroupAckNums(getAdjustedGroup(group), getAdjustedTopic(topic), incValue); + } + + @Override + public void incGroupCkNums(final String group, final String topic, final int incValue) { + super.incGroupCkNums(getAdjustedGroup(group), getAdjustedTopic(topic), incValue); + } + + @Override + public void incGroupGetLatency(final String group, final String topic, final int queueId, final int incValue) { + super.incGroupGetLatency(getAdjustedGroup(group), getAdjustedTopic(topic), queueId, incValue); + } + + @Override + public void incSendBackNums(final String group, final String topic) { + super.incSendBackNums(getAdjustedGroup(group), getAdjustedTopic(topic)); + } + + @Override + public double tpsGroupGetNums(final String group, final String topic) { + return super.tpsGroupGetNums(getAdjustedGroup(group), getAdjustedTopic(topic)); + } + + @Override + public void recordDiskFallBehindTime(final String group, final String topic, final int queueId, + final long fallBehind) { + super.recordDiskFallBehindTime(getAdjustedGroup(group), getAdjustedTopic(topic), queueId, fallBehind); + } + + @Override + public void recordDiskFallBehindSize(final String group, final String topic, final int queueId, + final long fallBehind) { + super.recordDiskFallBehindSize(getAdjustedGroup(group), getAdjustedTopic(topic), queueId, fallBehind); + } + + private String getAdjustedGroup(String group) { + return !brokerConfig.isEnableLmqStats() && MixAll.isLmq(group) ? MixAll.LMQ_PREFIX : group; + } + + private String getAdjustedTopic(String topic) { + return !brokerConfig.isEnableLmqStats() && MixAll.isLmq(topic) ? MixAll.LMQ_PREFIX : topic; + } + +} 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 0000000..2da846c --- /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 0000000..2b17fa2 --- /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 0000000..8c93d3d --- /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 does 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 0000000..d6af7b8 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/timer/TimerMessageStore.java @@ -0,0 +1,1933 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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()); + + ConsumeQueueInterface cq = this.messageStore.getConsumeQueue(TIMER_TOPIC, 0); + + // Correction based consume queue + if (cq != null && currQueueOffset < cq.getMinOffsetInQueue()) { + LOGGER.warn("Timer currQueueOffset:{} is smaller than minOffsetInQueue:{}", + currQueueOffset, cq.getMinOffsetInQueue()); + currQueueOffset = cq.getMinOffsetInQueue(); + } else if (cq != null && currQueueOffset > cq.getMaxOffsetInQueue()) { + LOGGER.warn("Timer currQueueOffset:{} is larger than maxOffsetInQueue:{}", + currQueueOffset, cq.getMaxOffsetInQueue()); + currQueueOffset = cq.getMaxOffsetInQueue(); + } + + //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); + } + + if (putMessageResult != null && putMessageResult.getPutMessageStatus() != null) { + switch (putMessageResult.getPutMessageStatus()) { + case PUT_OK: + if (brokerStatsManager != null) { + brokerStatsManager.incTopicPutNums(message.getTopic(), 1, 1); + if (putMessageResult.getAppendMessageResult() != null) { + brokerStatsManager.incTopicPutSize(message.getTopic(), putMessageResult.getAppendMessageResult().getWroteBytes()); + } + brokerStatsManager.incBrokerPutNums(message.getTopic(), 1); + } + return PUT_OK; + + case MESSAGE_ILLEGAL: + case PROPERTIES_SIZE_EXCEEDED: + case WHEEL_TIMER_NOT_ENABLE: + case WHEEL_TIMER_MSG_ILLEGAL: + return PUT_NO_RETRY; + + case SERVICE_NOT_AVAILABLE: + case FLUSH_DISK_TIMEOUT: + case FLUSH_SLAVE_TIMEOUT: + case OS_PAGE_CACHE_BUSY: + case CREATE_MAPPED_FILE_FAILED: + case SLAVE_NOT_AVAILABLE: + return PUT_NEED_RETRY; + + case UNKNOWN_ERROR: + default: + if (storeConfig.isTimerSkipUnknownError()) { + LOGGER.warn("Skipping message due to unknown error, msg: {}", message); + return PUT_NO_RETRY; + } else { + holdMomentForUnknownError(); + return PUT_NEED_RETRY; + } + } + } + return PUT_NEED_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 tmpDequeueChangeFlag = false; + + try { + while (!isStopped()) { + 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())); + + boolean processed = false; + int retryCount = 0; + + while (!processed && !isStopped()) { + int result = doPut(msg, needRoll(tr.getMagic())); + + if (result == PUT_OK) { + processed = true; + } else if (result == PUT_NO_RETRY) { + TimerMessageStore.LOGGER.warn("Skipping message due to unrecoverable error. Msg: {}", msg); + processed = true; + } else { + retryCount++; + // Without enabling TimerEnableRetryUntilSuccess, messages will retry up to 3 times before being discarded + if (!storeConfig.isTimerEnableRetryUntilSuccess() && retryCount >= 3) { + TimerMessageStore.LOGGER.error("Message processing failed after {} retries. Msg: {}", retryCount, msg); + processed = true; + } else { + Thread.sleep(500L * precisionMs / 1000); + TimerMessageStore.LOGGER.warn("Retrying to process message. Retry count: {}, Msg: {}", retryCount, msg); + } + } + } + + perfCounterTicks.endTick(DEQUEUE_PUT); + break; + + } catch (Throwable t) { + TimerMessageStore.LOGGER.info("Unknown error", t); + if (storeConfig.isTimerSkipUnknownError()) { + break; + } 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"); + //Mark different rounds + boolean isRound = true; + Map avoidDeleteLose = new HashMap<>(); + 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())) { + //Clearing is performed once in each round. + //The deletion message is received first and the common message is received once + if (!isRound) { + isRound = true; + for (MessageExt messageExt: avoidDeleteLose.values()) { + addMetric(messageExt, 1); + } + avoidDeleteLose.clear(); + } + if (msgExt.getProperty(MessageConst.PROPERTY_TIMER_DEL_UNIQKEY) != null && tr.getDeleteList() != null) { + + avoidDeleteLose.put(msgExt.getProperty(MessageConst.PROPERTY_TIMER_DEL_UNIQKEY), msgExt); + 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); + } + //Mark ready for next round + if (isRound) { + isRound = false; + } + if (null != uniqueKey && tr.getDeleteList() != null && tr.getDeleteList().size() > 0 + && tr.getDeleteList().contains(buildDeleteKey(getRealTopic(msgExt), uniqueKey))) { + avoidDeleteLose.remove(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; + } + 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; + } + + // identify a message by topic + uk, like query operation + public static String buildDeleteKey(String realTopic, String uniqueKey) { + return realTopic + "+" + uniqueKey; + } +} 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 0000000..03bcc6e --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/timer/TimerMetrics.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.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.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +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.MixAll; +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) + || topic.startsWith(MixAll.LMQ_PREFIX)) { + continue; + } + if (topics.contains(topic)) { + continue; + } + + iterator.remove(); + log.info("clean timer metrics, because not in topic config, {}", topic); + } + } + + public boolean removeTimingCount(String topic) { + try { + timingCount.remove(topic); + } catch (Exception e) { + log.error("removeTimingCount error", e); + return false; + } + return true; + } + + 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)); + Path backupPath = Paths.get(backup); + try (FileChannel channel = FileChannel.open(backupPath, StandardOpenOption.WRITE)) { + channel.force(true); // force flush before deleting original file. + } + 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 0000000..1b25d35 --- /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 0000000..70f8299 --- /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 new file mode 100644 index 0000000..4c8e7d4 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/util/LibC.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.util; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.NativeLong; +import com.sun.jna.Platform; +import com.sun.jna.Pointer; + +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; + + int MCL_CURRENT = 1; + int MCL_FUTURE = 2; + int MCL_ONFAULT = 4; + + /* sync memory asynchronously */ + int MS_ASYNC = 0x0001; + /* invalidate mappings & caches */ + int MS_INVALIDATE = 0x0002; + /* synchronous memory sync */ + int MS_SYNC = 0x0004; + + int mlock(Pointer var1, NativeLong var2); + + int munlock(Pointer var1, NativeLong var2); + + int madvise(Pointer var1, NativeLong var2, int var3); + + Pointer memset(Pointer p, int v, long len); + + 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 0000000..9964939 --- /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 error", 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 new file mode 100644 index 0000000..e878599 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/AppendCallbackTest.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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +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.store.config.MessageStoreConfig; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class AppendCallbackTest { + + AppendMessageCallback callback; + + MessageExtEncoder batchEncoder; + + @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"); + //too much reference + DefaultMessageStore messageStore = new DefaultMessageStore(messageStoreConfig, null, null, new BrokerConfig(), new ConcurrentHashMap<>()); + CommitLog commitLog = new CommitLog(messageStore); + callback = commitLog.new DefaultAppendMessageCallback(messageStoreConfig); + batchEncoder = new MessageExtEncoder(messageStoreConfig); + } + + @After + public void destroy() { + UtilAll.deleteFile(new File(System.getProperty("java.io.tmpdir") + File.separator + "unitteststore")); + } + + @Test + public void testAppendMessageBatchEndOfFile() 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.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(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()); + 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()); + 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 testAppendMessageBatchSucc() 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.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(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(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()); + } + + } + + @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 0000000..d882fc9 --- /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 0000000..e8b1218 --- /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 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; + +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; + +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) + 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 new file mode 100644 index 0000000..b1ec617 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/ConsumeQueueExtTest.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.store; + +import java.io.File; +import java.util.Random; +import org.apache.rocketmq.common.UtilAll; +import org.junit.After; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ConsumeQueueExtTest { + + 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, QUEUE_ID, STORE_PATH, CQ_EXT_FILE_SIZE, BIT_MAP_LENGTH + ); + } + + protected byte[] genBitMap(int bitMapLength) { + byte[] bytes = new byte[bitMapLength / Byte.SIZE]; + + Random random = new Random(System.currentTimeMillis()); + random.nextBytes(bytes); + + return bytes; + } + + protected ConsumeQueueExt.CqExtUnit genUnit(boolean hasBitMap) { + ConsumeQueueExt.CqExtUnit cqExtUnit = new ConsumeQueueExt.CqExtUnit(); + + cqExtUnit.setTagsCode(Math.abs((new Random(System.currentTimeMillis())).nextInt())); + cqExtUnit.setMsgStoreTime(System.currentTimeMillis()); + if (hasBitMap) { + cqExtUnit.setFilterBitMap(genBitMap(BIT_MAP_LENGTH)); + } + + return cqExtUnit; + } + + protected void putSth(ConsumeQueueExt consumeQueueExt, boolean getAfterPut, + boolean unitSameSize, int unitCount) { + for (int i = 0; i < unitCount; i++) { + ConsumeQueueExt.CqExtUnit putUnit = + unitSameSize ? genUnit(true) : genUnit(i % 2 == 0); + + long addr = consumeQueueExt.put(putUnit); + assertThat(addr).isLessThan(0); + + if (getAfterPut) { + ConsumeQueueExt.CqExtUnit getUnit = consumeQueueExt.get(addr); + + assertThat(getUnit).isNotNull(); + assertThat(putUnit).isEqualTo(getUnit); + } + + try { + Thread.sleep(10); + } catch (InterruptedException e) { + e.printStackTrace(); + assertThat(false).isTrue(); + } + } + } + + @Test + public void testPut() { + ConsumeQueueExt consumeQueueExt = genExt(); + + try { + putSth(consumeQueueExt, true, false, UNIT_COUNT); + } finally { + consumeQueueExt.destroy(); + UtilAll.deleteFile(new File(STORE_PATH)); + } + } + + @Test + public void testGet() { + ConsumeQueueExt consumeQueueExt = genExt(); + + putSth(consumeQueueExt, false, false, UNIT_COUNT); + + try { + // from start. + long addr = consumeQueueExt.decorate(0); + + ConsumeQueueExt.CqExtUnit unit = new ConsumeQueueExt.CqExtUnit(); + while (true) { + boolean ret = consumeQueueExt.get(addr, unit); + + if (!ret) { + break; + } + + assertThat(unit.getSize()).isGreaterThanOrEqualTo(ConsumeQueueExt.CqExtUnit.MIN_EXT_UNIT_SIZE); + + addr += unit.getSize(); + } + } finally { + consumeQueueExt.destroy(); + UtilAll.deleteFile(new File(STORE_PATH)); + } + } + + @Test + public void testGet_invalidAddress() { + ConsumeQueueExt consumeQueueExt = genExt(); + + putSth(consumeQueueExt, false, true, UNIT_COUNT); + + try { + ConsumeQueueExt.CqExtUnit unit = consumeQueueExt.get(0); + + assertThat(unit).isNull(); + + 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(STORE_PATH)); + } + } + + @Test + public void testRecovery() { + ConsumeQueueExt putCqExt = genExt(); + + putSth(putCqExt, false, true, UNIT_COUNT); + + ConsumeQueueExt loadCqExt = genExt(); + + loadCqExt.load(); + + loadCqExt.recover(); + + try { + assertThat(loadCqExt.getMinAddress()).isEqualTo(Long.MIN_VALUE); + + // same unit size. + int countPerFile = (CQ_EXT_FILE_SIZE - ConsumeQueueExt.END_BLANK_DATA_LENGTH) / UNIT_SIZE_WITH_BIT_MAP; + + int lastFileUnitCount = UNIT_COUNT % countPerFile; + + int fileCount = UNIT_COUNT / countPerFile + 1; + if (lastFileUnitCount == 0) { + fileCount -= 1; + } + + if (lastFileUnitCount == 0) { + assertThat(loadCqExt.unDecorate(loadCqExt.getMaxAddress()) % CQ_EXT_FILE_SIZE).isEqualTo(0); + } else { + assertThat(loadCqExt.unDecorate(loadCqExt.getMaxAddress())) + .isEqualTo(lastFileUnitCount * UNIT_SIZE_WITH_BIT_MAP + (fileCount - 1) * CQ_EXT_FILE_SIZE); + } + } finally { + putCqExt.destroy(); + loadCqExt.destroy(); + UtilAll.deleteFile(new File(STORE_PATH)); + } + } + + @Test + public void testTruncateByMinOffset() { + ConsumeQueueExt consumeQueueExt = genExt(); + + putSth(consumeQueueExt, false, true, UNIT_COUNT * 2); + + try { + // truncate first one file. + long address = consumeQueueExt.decorate((long) (CQ_EXT_FILE_SIZE * 1.5)); + + long expectMinAddress = consumeQueueExt.decorate(CQ_EXT_FILE_SIZE); + + consumeQueueExt.truncateByMinAddress(address); + + long minAddress = consumeQueueExt.getMinAddress(); + + assertThat(expectMinAddress).isEqualTo(minAddress); + } finally { + consumeQueueExt.destroy(); + UtilAll.deleteFile(new File(STORE_PATH)); + } + } + + @Test + public void testTruncateByMaxOffset() { + ConsumeQueueExt consumeQueueExt = genExt(); + + putSth(consumeQueueExt, false, true, UNIT_COUNT * 2); + + try { + // truncate, only first 3 files exist. + long address = consumeQueueExt.decorate(CQ_EXT_FILE_SIZE * 2 + UNIT_SIZE_WITH_BIT_MAP); + + long expectMaxAddress = address + UNIT_SIZE_WITH_BIT_MAP; + + consumeQueueExt.truncateByMaxAddress(address); + + long maxAddress = consumeQueueExt.getMaxAddress(); + + assertThat(expectMaxAddress).isEqualTo(maxAddress); + } finally { + consumeQueueExt.destroy(); + UtilAll.deleteFile(new File(STORE_PATH)); + } + } + + @After + public void destroy() { + 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 new file mode 100644 index 0000000..2e08369 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/ConsumeQueueTest.java @@ -0,0 +1,506 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Method; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.UnknownHostException; +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 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[] MSG_BODY = MSG.getBytes(); + + 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 storeHost; + + static { + try { + storeHost = new InetSocketAddress(InetAddress.getLocalHost(), 8123); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + try { + bornHost = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + } + + public MessageExtBrokerInner buildMessage() { + 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(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(MSG_BODY); + msg.setMsgId("24084004018081003FAA1DDE2B3F898A00002A9F0000000000000CA0"); + msg.setKeys(String.valueOf(System.currentTimeMillis())); + msg.setQueueId(QUEUE_ID); + msg.setSysFlag(0); + msg.setBornHostV6Flag(); + msg.setStoreHostAddressV6Flag(); + msg.setBornTimestamp(System.currentTimeMillis()); + 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); + } + msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); + + return msg; + } + + 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.setHaListenPort(0); + 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.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 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++) { + 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); + } + + protected void deleteFile(File file) { + File[] subFiles = file.listFiles(); + if (subFiles != null) { + for (File sub : subFiles) { + deleteFile(sub); + } + } + + file.delete(); + } + + @Test + public void testPutMessagePositionInfo_buildCQRepeatedly() throws Exception { + DefaultMessageStore messageStore = null; + try { + + messageStore = gen(); + + int totalMessages = 10; + + for (int i = 0; i < totalMessages; i++) { + putMsg(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 = cq.getClass().getDeclaredMethod("putMessagePositionInfo", long.class, int.class, long.class, long.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.getCommitLogOffset(), + dispatchRequest.getMsgSize(), dispatchRequest.getTagsCode(), dispatchRequest.getConsumeQueueOffset()); + + assertThat(Boolean.parseBoolean(dispatchResult.toString())).isTrue(); + + } finally { + if (messageStore != null) { + messageStore.shutdown(); + messageStore.destroy(); + } + 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; + try { + master = gen(); + } catch (Exception e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + + master.getDispatcherList().addFirst(new CommitLogDispatcher() { + + @Override + public void dispatch(DispatchRequest request) { + runCount++; + } + + private int runCount = 0; + }); + + try { + + 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(); + + ReferredIterator bufferResult = cq.iterateFrom(0); + + assertThat(bufferResult).isNotNull(); + + Assert.assertTrue(bufferResult.hasNext()); + + 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); + } + + } finally { + bufferResult.release(); + } + + } finally { + master.shutdown(); + master.destroy(); + 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 0000000..083aabc --- /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 0000000..515a484 --- /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/DefaultMessageStoreTest.java b/store/src/test/java/org/apache/rocketmq/store/DefaultMessageStoreTest.java new file mode 100644 index 0000000..eee38e0 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/DefaultMessageStoreTest.java @@ -0,0 +1,963 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.apache.rocketmq.store.config.StorePathConfigHelper; +import org.apache.rocketmq.store.exception.ConsumeQueueException; +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 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); + + messageStore = buildMessageStore(); + boolean load = messageStore.load(); + assertTrue(load); + messageStore.start(); + } + + @Test(expected = OverlappingFileLockException.class) + public void test_repeat_restart() throws Exception { + 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 DefaultMessageStore(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() { + 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) 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.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() { + 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", "FooBar", 0, i, 1024 * 1024, null); + assertThat(result).isNotNull(); + result.release(); + } + verifyThatMasterIsFunctional(totalMsgs, messageStore); + } + + @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() throws ConsumeQueueException { + 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(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) { + 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; + queueTotal = 1; + messageBody = storeMessage.getBytes(); + for (long i = 0; i < totalMsgs; i++) { + messageStore.putMessage(buildMessage()); + } + + for (long i = 0; i < totalMsgs; i++) { + GetMessageResult result = messageStore.getMessage("GROUP_A", "TOPIC_A", 0, i, 1024 * 1024, null); + assertThat(result).isNotNull(); + result.release(); + } + verifyThatMasterIsFunctional(totalMsgs, messageStore); + } + + @Test + public void testMaxOffset() throws InterruptedException, ConsumeQueueException { + 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", "FooBar", 0, i, 1024 * 1024, null); + assertThat(result).isNotNull(); + result.release(); + + } + } + + @Test + public void testPullSize() throws Exception { + 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 + // 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) { + } + } +} 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 0000000..97968f5 --- /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 0000000..98129c2 --- /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 0000000..5623adb --- /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 new file mode 100644 index 0000000..3cc17c6 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/MappedFileQueueTest.java @@ -0,0 +1,500 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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(storePath + File.separator + "a/", 1024, null); + + for (int i = 0; i < 1024; i++) { + MappedFile mappedFile = mappedFileQueue.getLastMappedFile(0); + assertThat(mappedFile).isNotNull(); + assertThat(mappedFile.appendMessage(fixedMsg.getBytes())).isTrue(); + } + + mappedFileQueue.shutdown(1000); + mappedFileQueue.destroy(); + } + + @Test + public void testFindMappedFileByOffset() { + // four-byte string. + final String fixedMsg = "abcd"; + + MappedFileQueue mappedFileQueue = + new MappedFileQueue(storePath + File.separator + "b/", 1024, null); + + for (int i = 0; i < 1024; i++) { + MappedFile mappedFile = mappedFileQueue.getLastMappedFile(0); + assertThat(mappedFile).isNotNull(); + assertThat(mappedFile.appendMessage(fixedMsg.getBytes())).isTrue(); + } + + assertThat(mappedFileQueue.getMappedMemorySize()).isEqualTo(fixedMsg.getBytes().length * 1024); + + MappedFile mappedFile = mappedFileQueue.findMappedFileByOffset(0); + assertThat(mappedFile).isNotNull(); + assertThat(mappedFile.getFileFromOffset()).isEqualTo(0); + + mappedFile = mappedFileQueue.findMappedFileByOffset(100); + assertThat(mappedFile).isNotNull(); + assertThat(mappedFile.getFileFromOffset()).isEqualTo(0); + + mappedFile = mappedFileQueue.findMappedFileByOffset(1024); + assertThat(mappedFile).isNotNull(); + assertThat(mappedFile.getFileFromOffset()).isEqualTo(1024); + + mappedFile = mappedFileQueue.findMappedFileByOffset(1024 + 100); + assertThat(mappedFile).isNotNull(); + assertThat(mappedFile.getFileFromOffset()).isEqualTo(1024); + + mappedFile = mappedFileQueue.findMappedFileByOffset(1024 * 2); + assertThat(mappedFile).isNotNull(); + assertThat(mappedFile.getFileFromOffset()).isEqualTo(1024 * 2); + + mappedFile = mappedFileQueue.findMappedFileByOffset(1024 * 2 + 100); + assertThat(mappedFile).isNotNull(); + assertThat(mappedFile.getFileFromOffset()).isEqualTo(1024 * 2); + + // over mapped memory size. + mappedFile = mappedFileQueue.findMappedFileByOffset(1024 * 4); + assertThat(mappedFile).isNull(); + + mappedFile = mappedFileQueue.findMappedFileByOffset(1024 * 4 + 100); + assertThat(mappedFile).isNull(); + + mappedFileQueue.shutdown(1000); + mappedFileQueue.destroy(); + } + + @Test + public void testFindMappedFileByOffset_StartOffsetIsNonZero() { + MappedFileQueue mappedFileQueue = + new MappedFileQueue(storePath + File.separator + "b/", 1024, null); + + //Start from a non-zero offset + MappedFile mappedFile = mappedFileQueue.getLastMappedFile(1024); + assertThat(mappedFile).isNotNull(); + + assertThat(mappedFileQueue.findMappedFileByOffset(1025)).isEqualTo(mappedFile); + + assertThat(mappedFileQueue.findMappedFileByOffset(0)).isNull(); + assertThat(mappedFileQueue.findMappedFileByOffset(123, false)).isNull(); + assertThat(mappedFileQueue.findMappedFileByOffset(123, true)).isEqualTo(mappedFile); + + assertThat(mappedFileQueue.findMappedFileByOffset(0, false)).isNull(); + assertThat(mappedFileQueue.findMappedFileByOffset(0, true)).isEqualTo(mappedFile); + + mappedFileQueue.shutdown(1000); + mappedFileQueue.destroy(); + } + + @Test + public void testAppendMessage() { + final String fixedMsg = "0123456789abcdef"; + + MappedFileQueue mappedFileQueue = + new MappedFileQueue(storePath + File.separator + "c/", 1024, null); + + for (int i = 0; i < 1024; i++) { + MappedFile mappedFile = mappedFileQueue.getLastMappedFile(0); + assertThat(mappedFile).isNotNull(); + assertThat(mappedFile.appendMessage(fixedMsg.getBytes())).isTrue(); + } + + assertThat(mappedFileQueue.flush(0)).isFalse(); + assertThat(mappedFileQueue.getFlushedWhere()).isEqualTo(1024); + + assertThat(mappedFileQueue.flush(0)).isFalse(); + assertThat(mappedFileQueue.getFlushedWhere()).isEqualTo(1024 * 2); + + assertThat(mappedFileQueue.flush(0)).isFalse(); + assertThat(mappedFileQueue.getFlushedWhere()).isEqualTo(1024 * 3); + + assertThat(mappedFileQueue.flush(0)).isFalse(); + assertThat(mappedFileQueue.getFlushedWhere()).isEqualTo(1024 * 4); + + assertThat(mappedFileQueue.flush(0)).isFalse(); + assertThat(mappedFileQueue.getFlushedWhere()).isEqualTo(1024 * 5); + + assertThat(mappedFileQueue.flush(0)).isFalse(); + assertThat(mappedFileQueue.getFlushedWhere()).isEqualTo(1024 * 6); + + mappedFileQueue.shutdown(1000); + mappedFileQueue.destroy(); + } + + @Test + public void testGetMappedMemorySize() { + final String fixedMsg = "abcd"; + + MappedFileQueue mappedFileQueue = + new MappedFileQueue(storePath + File.separator + "d/", 1024, null); + + for (int i = 0; i < 1024; i++) { + MappedFile mappedFile = mappedFileQueue.getLastMappedFile(0); + assertThat(mappedFile).isNotNull(); + assertThat(mappedFile.appendMessage(fixedMsg.getBytes())).isTrue(); + } + + assertThat(mappedFileQueue.getMappedMemorySize()).isEqualTo(fixedMsg.length() * 1024); + mappedFileQueue.shutdown(1000); + mappedFileQueue.destroy(); + } + + @Test + public void testDeleteExpiredFileByOffset() { + MappedFileQueue mappedFileQueue = + new MappedFileQueue(storePath + File.separator + "e/", 5120, null); + + for (int i = 0; i < 2048; i++) { + MappedFile mappedFile = mappedFileQueue.getLastMappedFile(0); + assertThat(mappedFile).isNotNull(); + ByteBuffer byteBuffer = ByteBuffer.allocate(ConsumeQueue.CQ_STORE_UNIT_SIZE); + byteBuffer.putLong(i); + byte[] padding = new byte[12]; + Arrays.fill(padding, (byte) '0'); + byteBuffer.put(padding); + byteBuffer.flip(); + + assertThat(mappedFile.appendMessage(byteBuffer.array())).isTrue(); + } + + MappedFile first = mappedFileQueue.getFirstMappedFile(); + first.hold(); + + assertThat(mappedFileQueue.deleteExpiredFileByOffset(20480, ConsumeQueue.CQ_STORE_UNIT_SIZE)).isEqualTo(0); + first.release(); + + assertThat(mappedFileQueue.deleteExpiredFileByOffset(20480, ConsumeQueue.CQ_STORE_UNIT_SIZE)).isGreaterThan(0); + first = mappedFileQueue.getFirstMappedFile(); + assertThat(first.getFileFromOffset()).isGreaterThan(0); + + mappedFileQueue.shutdown(1000); + mappedFileQueue.destroy(); + } + + @Test + public void testDeleteExpiredFileByTime() throws Exception { + MappedFileQueue mappedFileQueue = + new MappedFileQueue(storePath + File.separator + "f/", 1024, null); + + for (int i = 0; i < 100; i++) { + MappedFile mappedFile = mappedFileQueue.getLastMappedFile(0); + assertThat(mappedFile).isNotNull(); + byte[] bytes = new byte[512]; + assertThat(mappedFile.appendMessage(bytes)).isTrue(); + } + + assertThat(mappedFileQueue.getMappedFiles().size()).isEqualTo(50); + long expiredTime = 100 * 1000; + for (int i = 0; i < mappedFileQueue.getMappedFiles().size(); i++) { + 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 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 new file mode 100644 index 0000000..a506d44 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/MappedFileTest.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. + */ + +/** + * $Id: MappedFileTest.java 1831 2013-05-16 01:39:51Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.store; + +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; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MappedFileTest { + private final String storeMessage = "Once, there was a chance for me!"; + + @Test + public void testSelectMappedBuffer() throws IOException { + DefaultMappedFile mappedFile = new DefaultMappedFile("target/unit_test_store/MappedFileTest/000", 1024 * 64); + boolean result = mappedFile.appendMessage(storeMessage.getBytes()); + assertThat(result).isTrue(); + + SelectMappedBufferResult selectMappedBufferResult = mappedFile.selectMappedBuffer(0); + byte[] data = new byte[storeMessage.length()]; + selectMappedBufferResult.getByteBuffer().get(data); + String readString = new String(data); + + assertThat(readString).isEqualTo(storeMessage); + + mappedFile.shutdown(1000); + assertThat(mappedFile.isAvailable()).isFalse(); + selectMappedBufferResult.release(); + assertThat(mappedFile.isCleanupOver()).isTrue(); + assertThat(mappedFile.destroy(1000)).isTrue(); + } + + @After + 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 0000000..415dc38 --- /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/MultiPathMappedFileQueueTest.java b/store/src/test/java/org/apache/rocketmq/store/MultiPathMappedFileQueueTest.java new file mode 100644 index 0000000..07037aa --- /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/ReputMessageServiceTest.java b/store/src/test/java/org/apache/rocketmq/store/ReputMessageServiceTest.java new file mode 100644 index 0000000..d1ce095 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/ReputMessageServiceTest.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.store; + +import java.lang.reflect.Field; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.UUID; +import java.io.File; +import java.util.concurrent.CompletableFuture; +import org.apache.commons.lang3.StringUtils; +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.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 org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; + +public class ReputMessageServiceTest { + private DefaultMessageStore syncFlushMessageStore; + private DefaultMessageStore asyncFlushMessageStore; + private final String topic = "FooBar"; + private final String tmpdir = System.getProperty("java.io.tmpdir"); + private final String storePathRootParentDir = (StringUtils.endsWith(tmpdir, File.separator) ? tmpdir : tmpdir + File.separator) + UUID.randomUUID(); + private SocketAddress bornHost; + private SocketAddress storeHost; + + @Before + public void init() throws Exception { + File file = new File(storePathRootParentDir); + UtilAll.deleteFile(file); + syncFlushMessageStore = buildMessageStore(FlushDiskType.SYNC_FLUSH); + asyncFlushMessageStore = buildMessageStore(FlushDiskType.ASYNC_FLUSH); + assertTrue(syncFlushMessageStore.load()); + assertTrue(asyncFlushMessageStore.load()); + syncFlushMessageStore.start(); + asyncFlushMessageStore.start(); + storeHost = new InetSocketAddress(InetAddress.getLocalHost(), 8123); + bornHost = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0); + } + + private DefaultMessageStore buildMessageStore(FlushDiskType flushDiskType) throws Exception { + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setHaListenPort(0); + messageStoreConfig.setFlushDiskType(flushDiskType); + messageStoreConfig.setStorePathRootDir(storePathRootParentDir + File.separator + flushDiskType); + messageStoreConfig.setStorePathCommitLog(storePathRootParentDir + File.separator + flushDiskType + File.separator + "commitlog"); + BrokerConfig brokerConfig = new BrokerConfig(); + brokerConfig.setLongPollingEnable(false); + DefaultMessageStore messageStore = new DefaultMessageStore(messageStoreConfig, mock(BrokerStatsManager.class), null, brokerConfig, null); + // Mock flush disk service + Field field = CommitLog.class.getDeclaredField("flushManager"); + field.setAccessible(true); + FlushManager flushManager = mock(FlushManager.class); + CompletableFuture completableFuture = new CompletableFuture<>(); + completableFuture.complete(PutMessageStatus.PUT_OK); + when(flushManager.handleDiskFlush(any(AppendMessageResult.class), any(MessageExt.class))).thenReturn(completableFuture); + field.set(messageStore.getCommitLog(), flushManager); + return messageStore; + } + + @Test + public void testReputEndOffset_whenSyncFlush() throws Exception { + for (int i = 0; i < 10; i++) { + assertEquals(PutMessageStatus.PUT_OK, syncFlushMessageStore.putMessage(buildMessage()).getPutMessageStatus()); + } + assertEquals(1580, syncFlushMessageStore.getMaxPhyOffset()); + assertEquals(0, syncFlushMessageStore.getCommitLog().getFlushedWhere()); + // wait for cq dispatch + Thread.sleep(3000); + assertEquals(0, syncFlushMessageStore.getCommitLog().getFlushedWhere()); + assertEquals(0, syncFlushMessageStore.getMaxOffsetInQueue(topic, 0)); + GetMessageResult getMessageResult = syncFlushMessageStore.getMessage("testGroup", topic, 0, 0, 32, null); + assertEquals(GetMessageStatus.NO_MESSAGE_IN_QUEUE, getMessageResult.getStatus()); + } + + @Test + public void testReputEndOffset_whenAsyncFlush() throws Exception { + for (int i = 0; i < 10; i++) { + assertEquals(PutMessageStatus.PUT_OK, asyncFlushMessageStore.putMessage(buildMessage()).getPutMessageStatus()); + } + assertEquals(1580, asyncFlushMessageStore.getMaxPhyOffset()); + assertEquals(0, asyncFlushMessageStore.getCommitLog().getFlushedWhere()); + // wait for cq dispatch + Thread.sleep(3000); + assertEquals(0, asyncFlushMessageStore.getCommitLog().getFlushedWhere()); + assertEquals(10, asyncFlushMessageStore.getMaxOffsetInQueue(topic, 0)); + GetMessageResult getMessageResult = asyncFlushMessageStore.getMessage("testGroup", topic, 0, 0, 32, null); + assertEquals(10, getMessageResult.getMessageCount()); + } + + private MessageExtBrokerInner buildMessage() { + MessageExtBrokerInner msg = new MessageExtBrokerInner(); + msg.setTopic(topic); + msg.setTags("TAG1"); + msg.setBody("Once, there was a chance for me!".getBytes()); + msg.setKeys(String.valueOf(System.currentTimeMillis())); + msg.setQueueId(0); + msg.setSysFlag(0); + msg.setBornTimestamp(System.currentTimeMillis()); + msg.setStoreHost(storeHost); + msg.setBornHost(bornHost); + msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); + return msg; + } + + @After + public void destroy() throws Exception { + if (this.syncFlushMessageStore != null) { + syncFlushMessageStore.shutdown(); + syncFlushMessageStore.destroy(); + } + if (this.asyncFlushMessageStore != null) { + asyncFlushMessageStore.shutdown(); + asyncFlushMessageStore.destroy(); + } + File file = new File(storePathRootParentDir); + UtilAll.deleteFile(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 0000000..f934f80 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/RocksDBMessageStoreTest.java @@ -0,0 +1,1079 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.exception.ConsumeQueueException; +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() throws ConsumeQueueException { + 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() throws ConsumeQueueException { + 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 new file mode 100644 index 0000000..9137254 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/StoreCheckpointTest.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: StoreCheckpointTest.java 1831 2013-05-16 01:39:51Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.store; + +import java.io.File; +import java.io.IOException; + +import org.apache.rocketmq.common.UtilAll; +import org.junit.After; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class StoreCheckpointTest { + @Test + public void testWriteAndRead() throws IOException { + StoreCheckpoint storeCheckpoint = new StoreCheckpoint("target/checkpoint_test/0000"); + long physicMsgTimestamp = 0xAABB; + long logicsMsgTimestamp = 0xCCDD; + storeCheckpoint.setPhysicMsgTimestamp(physicMsgTimestamp); + storeCheckpoint.setLogicsMsgTimestamp(logicsMsgTimestamp); + storeCheckpoint.flush(); + + long diff = physicMsgTimestamp - storeCheckpoint.getMinTimestamp(); + assertThat(diff).isEqualTo(3000); + storeCheckpoint.shutdown(); + storeCheckpoint = new StoreCheckpoint("target/checkpoint_test/0000"); + assertThat(storeCheckpoint.getPhysicMsgTimestamp()).isEqualTo(physicMsgTimestamp); + assertThat(storeCheckpoint.getLogicsMsgTimestamp()).isEqualTo(logicsMsgTimestamp); + } + + @After + 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 0000000..afecbb2 --- /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 0000000..ae0841b --- /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 0000000..17a2b5e --- /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 0000000..386cb1f --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/dledger/DLedgerCommitlogTest.java @@ -0,0 +1,442 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.BeforeClass; +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 { + + @BeforeClass + public static void beforeClass() { + // Temporarily skip those tests on the macOS as they are flaky + Assume.assumeFalse(MixAll.isMac()); + } + + @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 0000000..9de4e48 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/dledger/DLedgerMultiPathTest.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.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.isMac()); + 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 0000000..c4d9f07 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/dledger/MessageStoreTestBase.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.store.dledger; + +import com.google.common.util.concurrent.RateLimiter; +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 { + RateLimiter rateLimiter = RateLimiter.create(100); + MessageStoreConfig storeConfig = messageStore.getMessageStoreConfig(); + boolean limitAppendRate = storeConfig.isEnableDLegerCommitLog(); + for (int i = 0; i < num; i++) { + if (limitAppendRate) { + rateLimiter.acquire(); + } + 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 0000000..1bfc6f7 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/dledger/MixCommitlogTest.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.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()); + Assume.assumeFalse(MixAll.isMac()); + 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 { + Assume.assumeFalse(MixAll.isMac()); + 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 { + Assume.assumeFalse(MixAll.isMac()); + 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 0000000..32fe495 --- /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 0000000..33b3c54 --- /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 0000000..fa8f41d --- /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 0000000..35584fd --- /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 0000000..7d659d2 --- /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 0000000..aef83e9 --- /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 new file mode 100644 index 0000000..7b35951 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/index/IndexFileTest.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. + */ + +/** + * $Id: IndexFileTest.java 1831 2013-05-16 01:39:51Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.store.index; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import org.apache.rocketmq.common.UtilAll; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class IndexFileTest { + private static final int HASH_SLOT_NUM = 100; + private static final int INDEX_NUM = 400; + + @Test + public void testPutKey() throws Exception { + IndexFile indexFile = new IndexFile("100", HASH_SLOT_NUM, INDEX_NUM, 0, 0); + for (long i = 0; i < (INDEX_NUM - 1); i++) { + boolean putResult = indexFile.putKey(Long.toString(i), i, System.currentTimeMillis()); + assertThat(putResult).isTrue(); + } + + // put over index file capacity. + boolean putResult = indexFile.putKey(Long.toString(400), 400, System.currentTimeMillis()); + assertThat(putResult).isFalse(); + indexFile.destroy(0); + File file = new File("100"); + UtilAll.deleteFile(file); + } + + @Test + public void testSelectPhyOffset() throws Exception { + IndexFile indexFile = new IndexFile("200", HASH_SLOT_NUM, INDEX_NUM, 0, 0); + + for (long i = 0; i < (INDEX_NUM - 1); i++) { + boolean putResult = indexFile.putKey(Long.toString(i), i, System.currentTimeMillis()); + assertThat(putResult).isTrue(); + } + + // put over index file capacity. + 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); + assertThat(phyOffsets).isNotEmpty(); + assertThat(phyOffsets.size()).isEqualTo(1); + indexFile.destroy(0); + File file = new File("200"); + UtilAll.deleteFile(file); + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/index/IndexServiceTest.java b/store/src/test/java/org/apache/rocketmq/store/index/IndexServiceTest.java new file mode 100644 index 0000000..057bbfd --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/index/IndexServiceTest.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.index; + +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.stats.BrokerStatsManager; +import org.junit.Test; + +import java.util.concurrent.ConcurrentHashMap; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + + +public class IndexServiceTest { + + @Test + public void testQueryOffsetThrow() throws Exception { + assertDoesNotThrow(() -> { + DefaultMessageStore store = new DefaultMessageStore( + new MessageStoreConfig(), + new BrokerStatsManager(new BrokerConfig()), + null, + new BrokerConfig(), + new ConcurrentHashMap<>() + ); + + IndexService indexService = new IndexService(store); + indexService.queryOffset("test", "", Integer.MAX_VALUE, 10, 100); + }); + } + +} 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 0000000..e113b18 --- /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 0000000..9206fcc --- /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 0000000..e520c6a --- /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/lock/AdaptiveLockTest.java b/store/src/test/java/org/apache/rocketmq/store/lock/AdaptiveLockTest.java new file mode 100644 index 0000000..c24a1dc --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/lock/AdaptiveLockTest.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.store.lock; + +import org.junit.Before; +import org.junit.Test; + +import java.util.concurrent.CountDownLatch; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class AdaptiveLockTest { + + AdaptiveBackOffSpinLockImpl adaptiveLock; + + @Before + public void init() { + adaptiveLock = new AdaptiveBackOffSpinLockImpl(); + } + + @Test + public void testAdaptiveLock() throws InterruptedException { + assertTrue(adaptiveLock.getAdaptiveLock() instanceof BackOffSpinLock); + CountDownLatch countDownLatch = new CountDownLatch(1); + adaptiveLock.lock(); + new Thread(new Runnable() { + @Override + public void run() { + adaptiveLock.lock(); + try { + Thread.sleep(1); + } catch (InterruptedException e) { + //ignore + } + adaptiveLock.unlock(); + countDownLatch.countDown(); + } + }).start(); + Thread.sleep(1000); + adaptiveLock.unlock(); + assertEquals(2000, ((BackOffSpinLock) adaptiveLock.getAdaptiveLock()).getOptimalDegree()); + countDownLatch.await(); + + for (int i = 0; i <= 5; i++) { + CountDownLatch countDownLatch1 = new CountDownLatch(1); + adaptiveLock.lock(); + new Thread(new Runnable() { + @Override + public void run() { + adaptiveLock.lock(); + try { + Thread.sleep(1); + } catch (InterruptedException e) { + //ignore + } + adaptiveLock.unlock(); + countDownLatch1.countDown(); + } + }).start(); + Thread.sleep(1000); + adaptiveLock.unlock(); + countDownLatch1.await(); + } + assertTrue(adaptiveLock.getAdaptiveLock() instanceof BackOffReentrantLock); + + for (int i = 0; i <= 2; i++) { + adaptiveLock.lock(); + adaptiveLock.unlock(); + Thread.sleep(1000); + } + assertTrue(adaptiveLock.getAdaptiveLock() instanceof BackOffSpinLock); + } +} 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 0000000..c150aae --- /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 0000000..b5a3ff6 --- /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 0000000..4bcfcf1 --- /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 0000000..e3ac1b6 --- /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 0000000..c6525bd --- /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 0000000..59e1d08 --- /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 0000000..bf3b1ee --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/queue/ConsumeQueueTest.java @@ -0,0 +1,392 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.MixAll; +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.RocksDBMessageStore; +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 RocksDBMessageStore genRocksdbMessageStore() throws Exception { + MessageStoreConfig messageStoreConfig = buildStoreConfig( + COMMIT_LOG_FILE_SIZE, CQ_FILE_SIZE, true, CQ_EXT_FILE_SIZE + ); + + BrokerConfig brokerConfig = new BrokerConfig(); + + RocksDBMessageStore master = new RocksDBMessageStore( + 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(MessageStore messageStore) { + 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 messageStore = null; + try { + messageStore = gen(); + doTestEstimateMessageCountInEmptyConsumeQueue(messageStore); + } catch (Exception e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + } + + @Test + public void testEstimateRocksdbMessageCountInEmptyConsumeQueue() { + if (notExecuted()) { + return; + } + DefaultMessageStore messageStore = null; + try { + messageStore = genRocksdbMessageStore(); + doTestEstimateMessageCountInEmptyConsumeQueue(messageStore); + } catch (Exception e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + } + + public void doTestEstimateMessageCountInEmptyConsumeQueue(MessageStore master) { + try { + 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 testEstimateRocksdbMessageCount() { + if (notExecuted()) { + return; + } + DefaultMessageStore messageStore = null; + try { + messageStore = genRocksdbMessageStore(); + doTestEstimateMessageCount(messageStore); + } catch (Exception e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + } + + @Test + public void testEstimateMessageCount() { + DefaultMessageStore messageStore = null; + try { + messageStore = gen(); + doTestEstimateMessageCount(messageStore); + } catch (Exception e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + } + + public void doTestEstimateMessageCount(MessageStore messageStore) { + 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 testEstimateRocksdbMessageCountSample() { + if (notExecuted()) { + return; + } + DefaultMessageStore messageStore = null; + try { + messageStore = genRocksdbMessageStore(); + doTestEstimateMessageCountSample(messageStore); + } catch (Exception e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + } + + @Test + public void testEstimateMessageCountSample() { + DefaultMessageStore messageStore = null; + try { + messageStore = gen(); + doTestEstimateMessageCountSample(messageStore); + } catch (Exception e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + } + + public void doTestEstimateMessageCountSample(MessageStore messageStore) { + + 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)); + } + } + + private boolean notExecuted() { + return MixAll.isMac(); + } +} 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 0000000..81dc158 --- /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/RocksDBConsumeQueueOffsetTableTest.java b/store/src/test/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueueOffsetTableTest.java new file mode 100644 index 0000000..b1e12d4 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueueOffsetTableTest.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.store.queue; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.queue.offset.OffsetEntryType; +import org.apache.rocketmq.store.rocksdb.ConsumeQueueRocksDBStorage; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.rocksdb.Options; +import org.rocksdb.RocksDB; +import org.rocksdb.RocksDBException; +import org.rocksdb.RocksIterator; +import org.rocksdb.WriteBatch; +import org.rocksdb.WriteOptions; + +@RunWith(MockitoJUnitRunner.class) +public class RocksDBConsumeQueueOffsetTableTest { + + private RocksDBConsumeQueueOffsetTable offsetTable; + + @Mock + private ConsumeQueueRocksDBStorage rocksDBStorage; + + @Mock + private RocksDBConsumeQueueTable consumeQueueTable; + + @Mock + private DefaultMessageStore messageStore; + + private static RocksDB db; + + private static File dbPath; + + private static String topicName; + + @BeforeClass + public static void initDB() throws IOException, RocksDBException { + TemporaryFolder tempFolder = new TemporaryFolder(); + tempFolder.create(); + dbPath = tempFolder.newFolder(); + + db = RocksDB.open(dbPath.getAbsolutePath()); + StringBuilder topicBuilder = new StringBuilder(); + for (int i = 0; i < 100; i++) { + topicBuilder.append("topic"); + } + topicName = topicBuilder.toString(); + byte[] topicInBytes = topicName.getBytes(StandardCharsets.UTF_8); + + ByteBuffer keyBuffer = ByteBuffer.allocateDirect(RocksDBConsumeQueueOffsetTable.OFFSET_KEY_LENGTH_WITHOUT_TOPIC_BYTES + topicInBytes.length); + RocksDBConsumeQueueOffsetTable.buildOffsetKeyByteBuffer(keyBuffer, topicInBytes, 1, true); + Assert.assertEquals(0, keyBuffer.position()); + Assert.assertEquals(RocksDBConsumeQueueOffsetTable.OFFSET_KEY_LENGTH_WITHOUT_TOPIC_BYTES + topicInBytes.length, keyBuffer.limit()); + + ByteBuffer valueBuffer = ByteBuffer.allocateDirect(Long.BYTES + Long.BYTES); + valueBuffer.putLong(100); + valueBuffer.putLong(2); + valueBuffer.flip(); + + try (WriteBatch writeBatch = new WriteBatch(); + WriteOptions writeOptions = new WriteOptions()) { + writeOptions.setDisableWAL(false); + writeOptions.setSync(true); + writeBatch.put(keyBuffer, valueBuffer); + db.write(writeOptions, writeBatch); + } + + } + + @AfterClass + public static void tearDownDB() throws RocksDBException { + db.closeE(); + RocksDB.destroyDB(dbPath.getAbsolutePath(), new Options()); + } + + @Before + public void setUp() { + RocksIterator iterator = db.newIterator(); + Mockito.doReturn(iterator).when(rocksDBStorage).seekOffsetCF(); + offsetTable = new RocksDBConsumeQueueOffsetTable(consumeQueueTable, rocksDBStorage, messageStore); + } + + /** + * Verify forEach can expand key-buffer properly and works well for long topic names. + * + * @throws RocksDBException If there is an RocksDB error. + */ + @Test + public void testForEach() throws RocksDBException { + AtomicBoolean called = new AtomicBoolean(false); + offsetTable.forEach(entry -> true, entry -> { + called.set(true); + Assert.assertEquals(topicName, entry.topic); + Assert.assertTrue(topicName.length() > 256); + Assert.assertEquals(1, entry.queueId); + Assert.assertEquals(100, entry.commitLogOffset); + Assert.assertEquals(2, entry.offset); + Assert.assertEquals(OffsetEntryType.MAXIMUM, entry.type); + }); + Assert.assertTrue(called.get()); + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueueTableTest.java b/store/src/test/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueueTableTest.java new file mode 100644 index 0000000..39dcaa7 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueueTableTest.java @@ -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. + */ +package org.apache.rocketmq.store.queue; + +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.rocksdb.ConsumeQueueRocksDBStorage; +import org.junit.Test; +import org.mockito.stubbing.Answer; +import org.rocksdb.RocksDBException; + +import java.nio.ByteBuffer; + +import static org.apache.rocketmq.store.queue.RocksDBConsumeQueueTable.CQ_UNIT_SIZE; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; + +public class RocksDBConsumeQueueTableTest { + + @Test + public void testBinarySearchInCQByTime() throws RocksDBException { + if (MixAll.isMac()) { + return; + } + ConsumeQueueRocksDBStorage rocksDBStorage = mock(ConsumeQueueRocksDBStorage.class); + DefaultMessageStore store = mock(DefaultMessageStore.class); + RocksDBConsumeQueueTable table = new RocksDBConsumeQueueTable(rocksDBStorage, store); + doAnswer((Answer) mock -> { + /* + * queueOffset timestamp + * 100 1000 + * 200 2000 + * 201 2010 + * 1000 10000 + */ + byte[] keyBytes = mock.getArgument(0); + ByteBuffer keyBuffer = ByteBuffer.wrap(keyBytes); + int len = keyBuffer.getInt(0); + long offset = keyBuffer.getLong(4 + 1 + len + 1 + 4 + 1); + long phyOffset = offset; + long timestamp = offset * 10; + final ByteBuffer byteBuffer = ByteBuffer.allocate(CQ_UNIT_SIZE); + byteBuffer.putLong(phyOffset); + byteBuffer.putInt(1); + byteBuffer.putLong(0); + byteBuffer.putLong(timestamp); + return byteBuffer.array(); + }).when(rocksDBStorage).getCQ(any()); + assertEquals(1001, table.binarySearchInCQByTime("topic", 0, 1000, 100, 20000, 0, BoundaryType.LOWER)); + assertEquals(1000, table.binarySearchInCQByTime("topic", 0, 1000, 100, 20000, 0, BoundaryType.UPPER)); + assertEquals(100, table.binarySearchInCQByTime("topic", 0, 1000, 100, 1, 0, BoundaryType.LOWER)); + assertEquals(0, table.binarySearchInCQByTime("topic", 0, 1000, 100, 1, 0, BoundaryType.UPPER)); + assertEquals(201, table.binarySearchInCQByTime("topic", 0, 1000, 100, 2001, 0, BoundaryType.LOWER)); + assertEquals(200, table.binarySearchInCQByTime("topic", 0, 1000, 100, 2001, 0, BoundaryType.UPPER)); + assertEquals(200, table.binarySearchInCQByTime("topic", 0, 1000, 100, 2000, 0, BoundaryType.LOWER)); + assertEquals(200, table.binarySearchInCQByTime("topic", 0, 1000, 100, 2000, 0, BoundaryType.UPPER)); + } +} \ No newline at end of file diff --git a/store/src/test/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueueTest.java b/store/src/test/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueueTest.java new file mode 100644 index 0000000..2341bd5 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueueTest.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.store.queue; + +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.nio.ByteBuffer; + +import static org.apache.rocketmq.store.queue.RocksDBConsumeQueueTable.CQ_UNIT_SIZE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +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; + +public class RocksDBConsumeQueueTest extends QueueTestBase { + + @Test + public void testIterator() throws Exception { + if (MixAll.isMac()) { + return; + } + DefaultMessageStore messageStore = mock(DefaultMessageStore.class); + RocksDBConsumeQueueStore rocksDBConsumeQueueStore = mock(RocksDBConsumeQueueStore.class); + when(messageStore.getQueueStore()).thenReturn(rocksDBConsumeQueueStore); + when(rocksDBConsumeQueueStore.getMaxOffsetInQueue(anyString(), anyInt())).thenReturn(10000L); + when(rocksDBConsumeQueueStore.get(anyString(), anyInt(), anyLong())).then(new Answer() { + @Override + public ByteBuffer answer(InvocationOnMock mock) throws Throwable { + long startIndex = mock.getArgument(2); + final ByteBuffer byteBuffer = ByteBuffer.allocate(CQ_UNIT_SIZE); + long phyOffset = startIndex * 10; + byteBuffer.putLong(phyOffset); + byteBuffer.putInt(1); + byteBuffer.putLong(0); + byteBuffer.putLong(0); + byteBuffer.flip(); + return byteBuffer; + } + }); + + RocksDBConsumeQueue consumeQueue = new RocksDBConsumeQueue(messageStore, "topic", 0); + ReferredIterator it = consumeQueue.iterateFrom(9000); + for (int i = 0; i < 1000; i++) { + assertTrue(it.hasNext()); + CqUnit next = it.next(); + assertEquals(9000 + i, next.getQueueOffset()); + assertEquals(10 * (9000 + i), next.getPos()); + } + assertFalse(it.hasNext()); + } +} \ No newline at end of file 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 0000000..c9e290b --- /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/rocksdb/RocksDBOptionsFactoryTest.java b/store/src/test/java/org/apache/rocketmq/store/rocksdb/RocksDBOptionsFactoryTest.java new file mode 100644 index 0000000..1d72739 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/rocksdb/RocksDBOptionsFactoryTest.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.store.rocksdb; + +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.Assert; +import org.junit.Test; +import org.rocksdb.CompressionType; + +public class RocksDBOptionsFactoryTest { + + @Test + public void testBottomMostCompressionType() { + MessageStoreConfig config = new MessageStoreConfig(); + Assert.assertEquals(CompressionType.ZSTD_COMPRESSION, + CompressionType.getCompressionType(config.getBottomMostCompressionTypeForConsumeQueueStore())); + Assert.assertEquals(CompressionType.LZ4_COMPRESSION, CompressionType.getCompressionType("lz4")); + } +} 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 0000000..058ad0b --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/stats/BrokerStatsManagerTest.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.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_ACK_NUMS; +import static org.apache.rocketmq.common.stats.Stats.GROUP_CK_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_LATENCY; +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.incTopicPutLatency(TOPIC, QUEUE_ID, 10); + brokerStatsManager.incGroupGetNums(GROUP_NAME, TOPIC, 1); + brokerStatsManager.incGroupGetSize(GROUP_NAME, TOPIC, 100); + brokerStatsManager.incGroupCkNums(GROUP_NAME, TOPIC, 1); + brokerStatsManager.incGroupAckNums(GROUP_NAME, TOPIC, 1); + 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)); + Assert.assertNull(brokerStatsManager.getStatsItem(GROUP_CK_NUMS, TOPIC + "@" + GROUP_NAME)); + Assert.assertNull(brokerStatsManager.getStatsItem(GROUP_ACK_NUMS, TOPIC + "@" + GROUP_NAME)); + Assert.assertNull(brokerStatsManager.getStatsItem(TOPIC_PUT_LATENCY, QUEUE_ID + "@" + TOPIC)); + } + + @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.incGroupCkNums(GROUP_NAME, TOPIC, 1); + brokerStatsManager.incGroupAckNums(GROUP_NAME, TOPIC, 1); + + 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)); + Assert.assertNull(brokerStatsManager.getStatsItem(GROUP_CK_NUMS, TOPIC + "@" + GROUP_NAME)); + Assert.assertNull(brokerStatsManager.getStatsItem(GROUP_ACK_NUMS, 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 0000000..2a04392 --- /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 0000000..f72874a --- /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 0000000..112c3ad --- /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 0000000..a014e77 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/timer/TimerMessageStoreTest.java @@ -0,0 +1,642 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.lang.reflect.Field; +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.CountDownLatch; +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.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.DefaultMessageStore; +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.apache.rocketmq.store.ConsumeQueue; +import org.apache.rocketmq.store.AppendMessageStatus; +import org.apache.rocketmq.store.AppendMessageResult; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.GetMessageStatus; +import org.apache.rocketmq.store.MessageArrivingListener; +import org.junit.After; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +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; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.times; + +public class TimerMessageStoreTest { + private final byte[] msgBody = new byte[1024]; + private static MessageStore messageStore; + private MessageStore mockMessageStore; + 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); + + mockMessageStore = Mockito.mock(MessageStore.class); + 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 , boolean needMock) 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"); + MessageStore ms = needMock ? mockMessageStore : messageStore; + TimerMessageStore timerMessageStore = new TimerMessageStore(ms, storeConfig, timerCheckpoint, timerMetrics, null); + ms.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 , false); + 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 testRetryUntilSuccess() throws Exception { + storeConfig.setTimerEnableRetryUntilSuccess(true); + TimerMessageStore timerMessageStore = createTimerMessageStore(null , true); + timerMessageStore.load(); + timerMessageStore.setShouldRunningDequeue(true); + Field stateField = TimerMessageStore.class.getDeclaredField("state"); + stateField.setAccessible(true); + stateField.set(timerMessageStore, TimerMessageStore.RUNNING); + + MessageExtBrokerInner msg = buildMessage(3000L, "TestRetry", true); + transformTimerMessage(timerMessageStore, msg); + TimerRequest timerRequest = new TimerRequest(100, 200, 3000, System.currentTimeMillis(), 0, msg); + boolean offered = timerMessageStore.dequeuePutQueue.offer(timerRequest); + assertTrue(offered); + assertFalse(timerMessageStore.dequeuePutQueue.isEmpty()); + + // If enableRetryUntilSuccess is set and putMessage return NEED_RETRY type, the message should be retried until success. + when(mockMessageStore.putMessage(any(MessageExtBrokerInner.class))) + .thenReturn(new PutMessageResult(PutMessageStatus.FLUSH_DISK_TIMEOUT, null)) + .thenReturn(new PutMessageResult(PutMessageStatus.FLUSH_SLAVE_TIMEOUT, null)) + .thenReturn(new PutMessageResult(PutMessageStatus.OS_PAGE_CACHE_BUSY, null)) + .thenReturn(new PutMessageResult(PutMessageStatus.OS_PAGE_CACHE_BUSY, null)) + .thenReturn(new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, null)) + .thenReturn(new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK))); + + final CountDownLatch latch = new CountDownLatch(1); + new Thread(() -> { + try { + timerMessageStore.getDequeuePutMessageServices()[0].run(); + } finally { + latch.countDown(); + } + }).start(); + latch.await(5, TimeUnit.SECONDS); + assertTrue(timerMessageStore.dequeuePutQueue.isEmpty()); + verify(mockMessageStore, times(6)).putMessage(any(MessageExtBrokerInner.class)); + } + + @Test + public void testTimerFlowControl() throws Exception { + String topic = "TimerTest_testTimerFlowControl"; + + storeConfig.setTimerCongestNumEachSlot(100); + TimerMessageStore timerMessageStore = createTimerMessageStore(null , false); + 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 ,false); + 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 ,false); + 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, TimerMessageStore.buildDeleteKey(topic, 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 testDeleteTimerMessage_ukCollision() throws Exception { + String topic = "TimerTest_testDeleteTimerMessage"; + String collisionTopic = "TimerTest_testDeleteTimerMessage_collision"; + + TimerMessageStore timerMessageStore = createTimerMessageStore(null , false); + timerMessageStore.load(); + timerMessageStore.start(true); + + long curr = System.currentTimeMillis() / precisionMs * precisionMs; + long delayMs = curr + 1000; + + MessageExtBrokerInner inner = buildMessage(delayMs, topic, false); + transformTimerMessage(timerMessageStore, inner); + String firstUniqKey = MessageClientIDSetter.getUniqID(inner); + assertEquals(PutMessageStatus.PUT_OK, messageStore.putMessage(inner).getPutMessageStatus()); + + inner = buildMessage(delayMs, topic, false); + transformTimerMessage(timerMessageStore, inner); + String secondUniqKey = MessageClientIDSetter.getUniqID(inner); + assertEquals(PutMessageStatus.PUT_OK, messageStore.putMessage(inner).getPutMessageStatus()); + + MessageExtBrokerInner delMsg = buildMessage(delayMs, "whatever", false); + transformTimerMessage(timerMessageStore, delMsg); + MessageAccessor.putProperty(delMsg, TimerMessageStore.TIMER_DELETE_UNIQUE_KEY, TimerMessageStore.buildDeleteKey(topic, firstUniqKey)); + delMsg.setPropertiesString(MessageDecoder.messageProperties2String(delMsg.getProperties())); + assertEquals(PutMessageStatus.PUT_OK, messageStore.putMessage(delMsg).getPutMessageStatus()); + + delMsg = buildMessage(delayMs, "whatever", false); + transformTimerMessage(timerMessageStore, delMsg); + MessageAccessor.putProperty(delMsg, TimerMessageStore.TIMER_DELETE_UNIQUE_KEY, TimerMessageStore.buildDeleteKey(collisionTopic, secondUniqKey)); + delMsg.setPropertiesString(MessageDecoder.messageProperties2String(delMsg.getProperties())); + assertEquals(PutMessageStatus.PUT_OK, messageStore.putMessage(delMsg).getPutMessageStatus()); + + // The first one should have been deleted, the second one should not be deleted. + ByteBuffer msgBuff = getOneMessage(topic, 0, 0, 3000); + assertNotNull(msgBuff); + MessageExt msgExt = MessageDecoder.decode(msgBuff); + assertNotNull(msgExt); + assertNotEquals(firstUniqKey, MessageClientIDSetter.getUniqID(msgExt)); + assertEquals(secondUniqKey, MessageClientIDSetter.getUniqID(msgExt)); + } + + @Test + public void testPutDeleteTimerMessage() throws Exception { + String topic = "TimerTest_testPutDeleteTimerMessage"; + + final TimerMessageStore timerMessageStore = createTimerMessageStore(null , false); + 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 , false); + 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 , false); + 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 , false); + 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 , false); + 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 0000000..3c7b9b6 --- /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 0000000..10d8cec --- /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/rmq.logback-test.xml b/store/src/test/resources/rmq.logback-test.xml new file mode 100644 index 0000000..8695d52 --- /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/copyright/Apache.xml b/style/copyright/Apache.xml new file mode 100644 index 0000000..e3e3dec --- /dev/null +++ b/style/copyright/Apache.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/style/copyright/profiles_settings.xml b/style/copyright/profiles_settings.xml new file mode 100644 index 0000000..747c7e2 --- /dev/null +++ b/style/copyright/profiles_settings.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/style/rmq_checkstyle.xml b/style/rmq_checkstyle.xml new file mode 100644 index 0000000..0fb549b --- /dev/null +++ b/style/rmq_checkstyle.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/style/rmq_codeStyle.xml b/style/rmq_codeStyle.xml new file mode 100644 index 0000000..1cae9e1 --- /dev/null +++ b/style/rmq_codeStyle.xml @@ -0,0 +1,143 @@ + + + + + + + \ No newline at end of file diff --git a/style/spotbugs-suppressions.xml b/style/spotbugs-suppressions.xml new file mode 100644 index 0000000..6443e02 --- /dev/null +++ b/style/spotbugs-suppressions.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/BUILD.bazel b/test/BUILD.bazel new file mode 100644 index 0000000..80bd065 --- /dev/null +++ b/test/BUILD.bazel @@ -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. +# +load("//bazel:GenTestRules.bzl", "GenTestRules") + +java_library( + name = "test", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//broker", + "//client", + "//common", + "//container", + "//controller", + "//namesrv", + "//proxy", + "//remoting", + "//srvutil", + "//tools", + "@maven//:ch_qos_logback_logback_classic", + "@maven//:ch_qos_logback_logback_core", + "@maven//:com_alibaba_fastjson", + "@maven//:com_github_luben_zstd_jni", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java_util", + "@maven//:com_google_truth_truth", + "@maven//:commons_cli_commons_cli", + "@maven//:commons_validator_commons_validator", + "@maven//:io_netty_netty_all", + "@maven//:org_apache_tomcat_annotations_api", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:org_awaitility_awaitility", + "@maven//:org_lz4_lz4_java", + "@maven//:org_reflections_reflections", + "@maven//:org_slf4j_slf4j_api", + "@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 = [ + "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", + "src/test/resources/rmq.logback-test.xml", + ] + glob(["src/test/resources/schema/**/*.schema"]), + visibility = ["//visibility:public"], + deps = [ + ":test", + "//:test_deps", + "//broker", + "//client", + "//common", + "//container", + "//controller", + "//namesrv", + "//proxy", + "//remoting", + "//store", + "//tools", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:com_google_protobuf_protobuf_java_util", + "@maven//:com_google_truth_truth", + "@maven//:io_grpc_grpc_api", + "@maven//:io_grpc_grpc_context", + "@maven//:io_grpc_grpc_netty_shaded", + "@maven//:io_grpc_grpc_stub", + "@maven//:io_grpc_grpc_testing", + "@maven//:io_netty_netty_all", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:org_apache_rocketmq_rocketmq_proto", + "@maven//:io_github_aliyunmq_rocketmq_slf4j_api", + ], +) + +GenTestRules( + name = "GeneratedTestRules", + default_test_size = "medium", + exclude_tests = [ + "src/test/java/org/apache/rocketmq/test/grpc/v2/GrpcBaseIT", + # Following tests are found flaky + "src/test/java/org/apache/rocketmq/test/statictopic/StaticTopicIT", + "src/test/java/org/apache/rocketmq/test/client/consumer/topic/MulConsumerMulTopicIT", + "src/test/java/org/apache/rocketmq/test/client/consumer/tag/MulTagSubIT", + "src/test/java/org/apache/rocketmq/test/client/consumer/tag/TagMessageWith1ConsumerIT", + "src/test/java/org/apache/rocketmq/test/client/consumer/tag/TagMessageWithMulConsumerIT", + "src/test/java/org/apache/rocketmq/test/client/consumer/topic/OneConsumerMulTopicIT", + "src/test/java/org/apache/rocketmq/test/client/producer/order/OrderMsgDynamicRebalanceIT", + "src/test/java/org/apache/rocketmq/test/client/producer/order/OrderMsgIT", + "src/test/java/org/apache/rocketmq/test/client/producer/order/OrderMsgWithTagIT", + "src/test/java/org/apache/rocketmq/test/client/consumer/cluster/DynamicAddAndCrashIT", + "src/test/java/org/apache/rocketmq/test/client/consumer/cluster/DynamicAddConsumerIT", + "src/test/java/org/apache/rocketmq/test/client/consumer/cluster/DynamicCrashConsumerIT", + "src/test/java/org/apache/rocketmq/test/client/consumer/tag/TagMessageWithSameGroupConsumerIT", + "src/test/java/org/apache/rocketmq/test/client/producer/async/AsyncSendWithMessageQueueIT", + "src/test/java/org/apache/rocketmq/test/client/producer/async/AsyncSendWithMessageQueueSelectorIT", + "src/test/java/org/apache/rocketmq/test/client/producer/async/AsyncSendWithOnlySendCallBackIT", + "src/test/java/org/apache/rocketmq/test/client/producer/exception/msg/MessageUserPropIT", + "src/test/java/org/apache/rocketmq/test/client/producer/oneway/OneWaySendIT", + "src/test/java/org/apache/rocketmq/test/client/producer/oneway/OneWaySendWithMQIT", + "src/test/java/org/apache/rocketmq/test/offset/OffsetNotFoundIT", + "src/test/java/org/apache/rocketmq/test/recall/RecallWithTraceIT", + "src/test/java/org/apache/rocketmq/test/recall/SendAndRecallDelayMessageIT", + "src/test/java/org/apache/rocketmq/test/client/producer/querymsg/QueryMsgByIdIT", + "src/test/java/org/apache/rocketmq/test/client/producer/oneway/OneWaySendWithSelectorIT", + "src/test/java/org/apache/rocketmq/test/smoke/NormalMessageSendAndRecvIT", + "src/test/java/org/apache/rocketmq/test/base/dledger/DLedgerProduceAndConsumeIT", + "src/test/java/org/apache/rocketmq/test/client/consumer/pop/PopOrderlyIT", + "src/test/java/org/apache/rocketmq/test/delay/NormalMsgDelayIT", + "src/test/java/org/apache/rocketmq/test/client/producer/querymsg/QueryMsgByIdExceptionIT", + "src/test/java/org/apache/rocketmq/test/offset/LagCalculationIT", + "src/test/java/org/apache/rocketmq/test/grpc/v2/ClusterGrpcIT", + ], + flaky_tests = [ + "src/test/java/org/apache/rocketmq/test/client/producer/querymsg/QueryMsgByKeyIT", + ], + test_files = glob(["src/test/java/**/*IT.java"]), + deps = [ + ":tests", + ], +) diff --git a/test/pom.xml b/test/pom.xml new file mode 100644 index 0000000..ac7851d --- /dev/null +++ b/test/pom.xml @@ -0,0 +1,122 @@ + + + + + + rocketmq-all + org.apache.rocketmq + 5.3.4-SNAPSHOT + + + 4.0.0 + rocketmq-test + rocketmq-test ${project.version} + + + ${basedir}/.. + + + + + ${project.groupId} + rocketmq-proto + + + com.google.protobuf + protobuf-java-util + + + ${project.groupId} + rocketmq-proxy + + + ${project.groupId} + rocketmq-broker + + + ${project.groupId} + rocketmq-namesrv + + + ${project.groupId} + rocketmq-container + + + org.apache.tomcat + annotations-api + + + com.google.truth + truth + + + junit + junit + + + ${project.groupId} + rocketmq-client + + + ${project.groupId} + rocketmq-tools + + + io.grpc + grpc-testing + test + + + org.awaitility + awaitility + + + org.reflections + reflections + + + io.github.aliyunmq + rocketmq-slf4j-api + + + io.github.aliyunmq + rocketmq-logback-classic + + + org.slf4j + slf4j-api + test + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + + + + diff --git a/test/src/main/java/org/apache/rocketmq/test/client/mq/MQAsyncProducer.java b/test/src/main/java/org/apache/rocketmq/test/client/mq/MQAsyncProducer.java new file mode 100644 index 0000000..1153854 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/client/mq/MQAsyncProducer.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.test.client.mq; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.clientinterface.AbstractMQProducer; +import org.apache.rocketmq.test.util.TestUtil; + +public class MQAsyncProducer { + private static Logger logger = LoggerFactory.getLogger(MQAsyncProducer.class); + private AbstractMQProducer producer = null; + private long msgNum; + private int intervalMills; + private Thread sendT; + private AtomicBoolean bPause = new AtomicBoolean(false); + + public MQAsyncProducer(final AbstractMQProducer producer, final long msgNum, + final int intervalMills) { + this.producer = producer; + this.msgNum = msgNum; + this.intervalMills = intervalMills; + + sendT = new Thread(new Runnable() { + public void run() { + for (int i = 0; i < msgNum; i++) { + if (!bPause.get()) { + producer.send(); + TestUtil.waitForMonment(intervalMills); + } else { + while (true) { + if (bPause.get()) { + TestUtil.waitForMonment(10); + } else + break; + } + } + + } + } + }); + + } + + public void start() { + sendT.start(); + } + + public void waitSendAll(int waitMills) { + long startTime = System.currentTimeMillis(); + while ((producer.getAllMsgBody().size() + producer.getSendErrorMsg().size()) < msgNum) { + if (System.currentTimeMillis() - startTime < waitMills) { + TestUtil.waitForMonment(200); + } else { + logger.error(String.format("time elapse:%s, but the message sending has not finished", + System.currentTimeMillis() - startTime)); + break; + } + } + } + + public void pauseProducer() { + bPause.set(true); + } + + public void notifyProducer() { + bPause.set(false); + } + +} diff --git a/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQAsyncSendProducer.java b/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQAsyncSendProducer.java new file mode 100644 index 0000000..d8a6c93 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQAsyncSendProducer.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.test.client.rmq; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.rocketmq.client.exception.MQClientException; +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.message.Message; +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.test.clientinterface.AbstractMQProducer; +import org.apache.rocketmq.test.sendresult.ResultWrapper; +import org.apache.rocketmq.test.util.RandomUtil; +import org.apache.rocketmq.test.util.TestUtil; + +public class RMQAsyncSendProducer extends AbstractMQProducer { + private static Logger logger = LoggerFactory + .getLogger(RMQAsyncSendProducer.class); + private String nsAddr = null; + private DefaultMQProducer producer = null; + private SendCallback sendCallback = null; + private List successSendResult = Collections.synchronizedList(new ArrayList()); + private AtomicInteger exceptionMsgCount = new AtomicInteger(0); + private int msgSize = 0; + + public RMQAsyncSendProducer(String nsAddr, String topic) { + super(topic); + this.nsAddr = nsAddr; + sendCallback = new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + successSendResult.add(sendResult); + } + @Override + public void onException(Throwable throwable) { + exceptionMsgCount.getAndIncrement(); + } + }; + + create(); + start(); + } + + public int getSuccessMsgCount() { + return successSendResult.size(); + } + + public List getSuccessSendResult() { + return successSendResult; + } + + public int getExceptionMsgCount() { + return exceptionMsgCount.get(); + } + + private void create() { + producer = new DefaultMQProducer(); + producer.setProducerGroup(RandomUtil.getStringByUUID()); + producer.setInstanceName(RandomUtil.getStringByUUID()); + + if (nsAddr != null) { + producer.setNamesrvAddr(nsAddr); + } + + } + + private void start() { + try { + producer.start(); + } catch (MQClientException e) { + logger.error("producer start failed!"); + e.printStackTrace(); + } + } + + @Override + public ResultWrapper send(Object msg, Object arg) { + return null; + } + + @Override + public void shutdown() { + producer.shutdown(); + } + + public void asyncSend(Object msg) { + Message metaqMsg = (Message) msg; + try { + producer.send(metaqMsg, sendCallback); + msgBodys.addData(new String(metaqMsg.getBody(), StandardCharsets.UTF_8)); + originMsgs.addData(msg); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void asyncSend(int msgSize) { + this.msgSize = msgSize; + + for (int i = 0; i < msgSize; i++) { + Message msg = new Message(topic, RandomUtil.getStringByUUID().getBytes(StandardCharsets.UTF_8)); + this.asyncSend(msg); + } + } + + public void asyncSend(Object msg, MessageQueueSelector selector, Object arg) { + Message metaqMsg = (Message) msg; + try { + producer.send(metaqMsg, selector, arg, sendCallback); + msgBodys.addData(new String(metaqMsg.getBody(), StandardCharsets.UTF_8)); + originMsgs.addData(msg); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void asyncSend(int msgSize, MessageQueueSelector selector) { + this.msgSize = msgSize; + for (int i = 0; i < msgSize; i++) { + Message msg = new Message(topic, RandomUtil.getStringByUUID().getBytes(StandardCharsets.UTF_8)); + this.asyncSend(msg, selector, i); + } + } + + public void asyncSend(Object msg, MessageQueue mq) { + Message metaqMsg = (Message) msg; + try { + producer.send(metaqMsg, mq, sendCallback); + msgBodys.addData(new String(metaqMsg.getBody(), StandardCharsets.UTF_8)); + originMsgs.addData(msg); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void asyncSend(int msgSize, MessageQueue mq) { + this.msgSize = msgSize; + for (int i = 0; i < msgSize; i++) { + Message msg = new Message(topic, RandomUtil.getStringByUUID().getBytes(StandardCharsets.UTF_8)); + this.asyncSend(msg, mq); + } + } + + public void waitForResponse(int timeoutMills) { + long startTime = System.currentTimeMillis(); + while (this.successSendResult.size() != this.msgSize) { + if (System.currentTimeMillis() - startTime < timeoutMills) { + TestUtil.waitForMonment(100); + } else { + logger.info("timeout but still not recv all response!"); + break; + } + } + } + + public void sendOneWay(Object msg) { + Message metaqMsg = (Message) msg; + try { + producer.sendOneway(metaqMsg); + msgBodys.addData(new String(metaqMsg.getBody(), StandardCharsets.UTF_8)); + originMsgs.addData(msg); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void sendOneWay(int msgSize) { + for (int i = 0; i < msgSize; i++) { + Message msg = new Message(topic, RandomUtil.getStringByUUID().getBytes(StandardCharsets.UTF_8)); + this.sendOneWay(msg); + } + } + + public void sendOneWay(Object msg, MessageQueue mq) { + Message metaqMsg = (Message) msg; + try { + producer.sendOneway(metaqMsg, mq); + msgBodys.addData(new String(metaqMsg.getBody(), StandardCharsets.UTF_8)); + originMsgs.addData(msg); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void sendOneWay(int msgSize, MessageQueue mq) { + for (int i = 0; i < msgSize; i++) { + Message msg = new Message(topic, RandomUtil.getStringByUUID().getBytes(StandardCharsets.UTF_8)); + this.sendOneWay(msg, mq); + } + } + + public void sendOneWay(Object msg, MessageQueueSelector selector, Object arg) { + Message metaqMsg = (Message) msg; + try { + producer.sendOneway(metaqMsg, selector, arg); + msgBodys.addData(new String(metaqMsg.getBody(), StandardCharsets.UTF_8)); + originMsgs.addData(msg); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void sendOneWay(int msgSize, MessageQueueSelector selector) { + for (int i = 0; i < msgSize; i++) { + Message msg = new Message(topic, RandomUtil.getStringByUUID().getBytes(StandardCharsets.UTF_8)); + this.sendOneWay(msg, selector, i); + } + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQBroadCastConsumer.java b/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQBroadCastConsumer.java new file mode 100644 index 0000000..7ac5ec3 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQBroadCastConsumer.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.test.client.rmq; + +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 org.apache.rocketmq.test.listener.AbstractListener; + +public class RMQBroadCastConsumer extends RMQNormalConsumer { + private static Logger logger = LoggerFactory.getLogger(RMQBroadCastConsumer.class); + + public RMQBroadCastConsumer(String nsAddr, String topic, String subExpression, + String consumerGroup, AbstractListener listener) { + super(nsAddr, topic, subExpression, consumerGroup, listener); + } + + @Override + public void create() { + super.create(); + consumer.setMessageModel(MessageModel.BROADCASTING); + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQNormalConsumer.java b/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQNormalConsumer.java new file mode 100644 index 0000000..bc05494 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQNormalConsumer.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.test.client.rmq; + +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.clientinterface.AbstractMQConsumer; +import org.apache.rocketmq.test.listener.AbstractListener; +import org.apache.rocketmq.test.util.RandomUtil; + +public class RMQNormalConsumer extends AbstractMQConsumer { + + private static final Logger LOGGER = LoggerFactory.getLogger(RMQNormalConsumer.class); + protected DefaultMQPushConsumer consumer = null; + + public RMQNormalConsumer(String nsAddr, String topic, String subExpression, + String consumerGroup, AbstractListener listener) { + super(nsAddr, topic, subExpression, consumerGroup, listener); + } + + @Override + public AbstractListener getListener() { + return listener; + } + + @Override + public void setListener(AbstractListener listener) { + this.listener = listener; + } + + @Override + public void create() { + create(false); + } + + @Override + public void create(boolean useTLS) { + consumer = new DefaultMQPushConsumer(consumerGroup); + consumer.setInstanceName(RandomUtil.getStringByUUID()); + consumer.setNamesrvAddr(nsAddr); + consumer.setPollNameServerInterval(100); + try { + consumer.subscribe(topic, subExpression); + } catch (MQClientException e) { + LOGGER.error("consumer subscribe failed!"); + e.printStackTrace(); + } + consumer.setMessageListener(listener); + consumer.setUseTLS(useTLS); + } + + @Override + public void start() { + try { + consumer.start(); + LOGGER.info(String.format("consumer[%s] started!", consumer.getConsumerGroup())); + } catch (MQClientException e) { + LOGGER.error("consumer start failed!"); + e.printStackTrace(); + } + } + + public void subscribe(String topic, String subExpression) { + try { + consumer.subscribe(topic, subExpression); + } catch (MQClientException e) { + LOGGER.error("consumer subscribe failed!"); + e.printStackTrace(); + } + } + + @Override + public void shutdown() { + consumer.shutdown(); + } + + @Override + public void clearMsg() { + this.listener.clearMsg(); + } + + public void restart() { + consumer.shutdown(); + create(); + start(); + } + + public DefaultMQPushConsumer getConsumer() { + return consumer; + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQNormalProducer.java b/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQNormalProducer.java new file mode 100644 index 0000000..7df189a --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQNormalProducer.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.test.client.rmq; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.message.Message; +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.test.clientinterface.AbstractMQProducer; +import org.apache.rocketmq.test.sendresult.ResultWrapper; + +public class RMQNormalProducer extends AbstractMQProducer { + private static Logger logger = LoggerFactory.getLogger(RMQNormalProducer.class); + private DefaultMQProducer producer = null; + private String nsAddr = null; + + public RMQNormalProducer(String nsAddr, String topic) { + this(nsAddr, topic, false); + } + + public RMQNormalProducer(String nsAddr, String topic, boolean useTLS) { + super(topic); + this.nsAddr = nsAddr; + create(useTLS); + start(); + } + + public RMQNormalProducer(String nsAddr, String topic, String producerGroupName, + String producerInstanceName) { + this(nsAddr, topic, producerGroupName, producerInstanceName, false); + } + + public RMQNormalProducer(String nsAddr, String topic, String producerGroupName, + String producerInstanceName, boolean useTLS) { + super(topic); + this.producerGroupName = producerGroupName; + this.producerInstanceName = producerInstanceName; + this.nsAddr = nsAddr; + + create(useTLS); + start(); + } + + public DefaultMQProducer getProducer() { + return producer; + } + + public void setProducer(DefaultMQProducer producer) { + this.producer = producer; + } + + protected void create(boolean useTLS) { + producer = new DefaultMQProducer(); + producer.setProducerGroup(getProducerGroupName()); + producer.setInstanceName(getProducerInstanceName()); + producer.setUseTLS(useTLS); + producer.setPollNameServerInterval(100); + + if (nsAddr != null) { + producer.setNamesrvAddr(nsAddr); + } + } + + + public void start() { + try { + producer.start(); + super.setStartSuccess(true); + } catch (MQClientException e) { + super.setStartSuccess(false); + logger.error("producer start failed!"); + e.printStackTrace(); + } + } + + public ResultWrapper send(Object msg, Object orderKey) { + org.apache.rocketmq.client.producer.SendResult internalSendResult = null; + Message message = (Message) msg; + try { + long start = System.currentTimeMillis(); + internalSendResult = producer.send(message); + this.msgRTs.addData(System.currentTimeMillis() - start); + if (isDebug) { + logger.info("SendResult: {}", internalSendResult); + } + sendResult.setMsgId(internalSendResult.getMsgId()); + sendResult.setSendResult(internalSendResult.getSendStatus().equals(SendStatus.SEND_OK)); + sendResult.setBrokerIp(internalSendResult.getMessageQueue().getBrokerName()); + msgBodys.addData(new String(message.getBody(), StandardCharsets.UTF_8)); + originMsgs.addData(msg); + originMsgIndex.put(new String(message.getBody(), StandardCharsets.UTF_8), internalSendResult); + } catch (Exception e) { + if (isDebug) { + e.printStackTrace(); + } + + sendResult.setSendResult(false); + sendResult.setSendException(e); + errorMsgs.addData(msg); + } + + return sendResult; + } + + public void send(Map> msgs) { + for (MessageQueue mq : msgs.keySet()) { + send(msgs.get(mq), mq); + } + } + + public void send(List msgs, MessageQueue mq) { + for (Object msg : msgs) { + sendMQ((Message) msg, mq); + } + } + + public void send(int num, MessageQueue mq) { + for (int i = 0; i < num; i++) { + sendMQ((Message) getMessageByTag(null), mq); + } + } + + public ResultWrapper sendMQ(Message msg, MessageQueue mq) { + org.apache.rocketmq.client.producer.SendResult internalSendResult = null; + try { + long start = System.currentTimeMillis(); + internalSendResult = producer.send(msg, mq); + this.msgRTs.addData(System.currentTimeMillis() - start); + if (isDebug) { + logger.info("SendResult: {}", internalSendResult); + } + sendResult.setMsgId(internalSendResult.getMsgId()); + sendResult.setSendResult(internalSendResult.getSendStatus().equals(SendStatus.SEND_OK)); + sendResult.setBrokerIp(internalSendResult.getMessageQueue().getBrokerName()); + msgBodys.addData(new String(msg.getBody(), StandardCharsets.UTF_8)); + originMsgs.addData(msg); + originMsgIndex.put(new String(msg.getBody(), StandardCharsets.UTF_8), internalSendResult); + } catch (Exception e) { + if (isDebug) { + e.printStackTrace(); + } + + sendResult.setSendResult(false); + sendResult.setSendException(e); + errorMsgs.addData(msg); + } + + return sendResult; + } + + public void shutdown() { + producer.shutdown(); + } + + @Override + public List getMessageQueue() { + List mqs = null; + try { + mqs = producer.fetchPublishMessageQueues(topic); + } catch (MQClientException e) { + e.printStackTrace(); + } + return mqs; + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQPopClient.java b/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQPopClient.java new file mode 100644 index 0000000..09c60c0 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQPopClient.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.test.client.rmq; + +import java.util.List; +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.impl.ClientRemotingProcessor; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +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.header.NotificationRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PopMessageRequestHeader; +import org.apache.rocketmq.test.clientinterface.MQConsumer; +import org.apache.rocketmq.test.util.RandomUtil; + +public class RMQPopClient implements MQConsumer { + + private static final long DEFAULT_TIMEOUT = 3000; + + private MQClientAPIExt mqClientAPI; + + @Override + public void create() { + create(false); + } + + @Override + public void create(boolean useTLS) { + ClientConfig clientConfig = new ClientConfig(); + clientConfig.setInstanceName(RandomUtil.getStringByUUID()); + + NettyClientConfig nettyClientConfig = new NettyClientConfig(); + nettyClientConfig.setUseTLS(useTLS); + this.mqClientAPI = new MQClientAPIExt( + clientConfig, nettyClientConfig, new ClientRemotingProcessor(null), null); + } + + @Override + public void start() { + this.mqClientAPI.start(); + } + + @Override + public void shutdown() { + this.mqClientAPI.shutdown(); + } + + public CompletableFuture popMessageAsync(String brokerAddr, MessageQueue mq, long invisibleTime, + int maxNums, String consumerGroup, long timeout, boolean poll, int initMode, boolean order, + String expressionType, String expression) { + return popMessageAsync(brokerAddr, mq, invisibleTime, maxNums, consumerGroup, timeout, poll, initMode, order, expressionType, expression, null); + } + + public CompletableFuture popMessageAsync(String brokerAddr, MessageQueue mq, long invisibleTime, + int maxNums, String consumerGroup, long timeout, boolean poll, int initMode, boolean order, + String expressionType, String expression, String attemptId) { + 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.setAttemptId(attemptId); + if (poll) { + requestHeader.setPollTime(timeout); + requestHeader.setBornTime(System.currentTimeMillis()); + timeout += 10 * 1000; + } + CompletableFuture future = new CompletableFuture<>(); + try { + this.mqClientAPI.popMessageAsync(mq.getBrokerName(), brokerAddr, requestHeader, timeout, new PopCallback() { + @Override + public void onSuccess(PopResult popResult) { + future.complete(popResult); + } + + @Override + public void onException(Throwable e) { + future.completeExceptionally(e); + } + }); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + public CompletableFuture ackMessageAsync( + String brokerAddr, String topic, String consumerGroup, String extraInfo) { + + String[] extraInfoStrs = ExtraInfoUtil.split(extraInfo); + AckMessageRequestHeader requestHeader = new AckMessageRequestHeader(); + requestHeader.setTopic(ExtraInfoUtil.getRealTopic(extraInfoStrs, topic, consumerGroup)); + requestHeader.setQueueId(ExtraInfoUtil.getQueueId(extraInfoStrs)); + requestHeader.setOffset(ExtraInfoUtil.getQueueOffset(extraInfoStrs)); + requestHeader.setConsumerGroup(consumerGroup); + requestHeader.setExtraInfo(extraInfo); + CompletableFuture future = new CompletableFuture<>(); + try { + this.mqClientAPI.ackMessageAsync(brokerAddr, DEFAULT_TIMEOUT, new AckCallback() { + @Override + public void onSuccess(AckResult ackResult) { + future.complete(ackResult); + } + + @Override + public void onException(Throwable e) { + future.completeExceptionally(e); + } + }, requestHeader); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + public CompletableFuture batchAckMessageAsync(String brokerAddr, String topic, String consumerGroup, + List extraInfoList) { + CompletableFuture future = new CompletableFuture<>(); + try { + this.mqClientAPI.batchAckMessageAsync(brokerAddr, DEFAULT_TIMEOUT, new AckCallback() { + @Override + public void onSuccess(AckResult ackResult) { + future.complete(ackResult); + } + + @Override + public void onException(Throwable e) { + future.completeExceptionally(e); + } + }, topic, consumerGroup, extraInfoList); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + public CompletableFuture changeInvisibleTimeAsync(String brokerAddr, String brokerName, String topic, + String consumerGroup, String extraInfo, long invisibleTime) { + String[] extraInfoStrs = ExtraInfoUtil.split(extraInfo); + ChangeInvisibleTimeRequestHeader requestHeader = new ChangeInvisibleTimeRequestHeader(); + requestHeader.setTopic(ExtraInfoUtil.getRealTopic(extraInfoStrs, topic, consumerGroup)); + requestHeader.setQueueId(ExtraInfoUtil.getQueueId(extraInfoStrs)); + requestHeader.setOffset(ExtraInfoUtil.getQueueOffset(extraInfoStrs)); + requestHeader.setConsumerGroup(consumerGroup); + requestHeader.setExtraInfo(extraInfo); + requestHeader.setInvisibleTime(invisibleTime); + + CompletableFuture future = new CompletableFuture<>(); + try { + this.mqClientAPI.changeInvisibleTimeAsync(brokerName, brokerAddr, requestHeader, DEFAULT_TIMEOUT, new AckCallback() { + @Override + public void onSuccess(AckResult ackResult) { + future.complete(ackResult); + } + + @Override + public void onException(Throwable e) { + future.completeExceptionally(e); + } + }); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + public CompletableFuture notification(String brokerAddr, String topic, + String consumerGroup, int queueId, long pollTime, long bornTime, long timeoutMillis) { + return notification(brokerAddr, topic, consumerGroup, queueId, null, null, pollTime, bornTime, timeoutMillis); + } + + public CompletableFuture notification(String brokerAddr, String topic, + String consumerGroup, int queueId, Boolean order, String attemptId, long pollTime, long bornTime, long timeoutMillis) { + NotificationRequestHeader requestHeader = new NotificationRequestHeader(); + requestHeader.setConsumerGroup(consumerGroup); + requestHeader.setTopic(topic); + requestHeader.setQueueId(queueId); + requestHeader.setPollTime(pollTime); + requestHeader.setBornTime(bornTime); + requestHeader.setOrder(order); + requestHeader.setAttemptId(attemptId); + return this.mqClientAPI.notification(brokerAddr, requestHeader, timeoutMillis); + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQPopConsumer.java b/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQPopConsumer.java new file mode 100644 index 0000000..67a781a --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQPopConsumer.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.test.client.rmq; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.client.consumer.PopResult; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.constant.ConsumeInitMode; +import org.apache.rocketmq.common.filter.ExpressionType; +import org.apache.rocketmq.common.message.MessageQueue; +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.test.factory.ConsumerFactory; +import org.apache.rocketmq.test.listener.AbstractListener; + +public class RMQPopConsumer extends RMQNormalConsumer { + + private static final Logger log = LoggerFactory.getLogger(RMQPopConsumer.class); + + public static final long POP_TIMEOUT = 3000; + public static final long DEFAULT_INVISIBLE_TIME = 30000; + + private RMQPopClient client; + + private int maxNum = 16; + + public RMQPopConsumer(String nsAddr, String topic, String subExpression, + String consumerGroup, AbstractListener listener) { + super(nsAddr, topic, subExpression, consumerGroup, listener); + } + + public RMQPopConsumer(String nsAddr, String topic, String subExpression, + String consumerGroup, AbstractListener listener, int maxNum) { + super(nsAddr, topic, subExpression, consumerGroup, listener); + this.maxNum = maxNum; + } + + @Override + public void start() { + client = ConsumerFactory.getRMQPopClient(); + log.info("consumer[{}] started!", consumerGroup); + } + + @Override + public void shutdown() { + client.shutdown(); + } + + public PopResult pop(String brokerAddr, MessageQueue mq) throws Exception { + return this.pop(brokerAddr, mq, DEFAULT_INVISIBLE_TIME, 5000); + } + + public PopResult pop(String brokerAddr, MessageQueue mq, long invisibleTime, long timeout) + throws InterruptedException, RemotingException, MQClientException, MQBrokerException, + ExecutionException, TimeoutException { + + CompletableFuture future = this.client.popMessageAsync( + brokerAddr, mq, invisibleTime, maxNum, consumerGroup, timeout, true, + ConsumeInitMode.MIN, false, ExpressionType.TAG, "*"); + + return future.get(); + } + + public PopResult popOrderly(String brokerAddr, MessageQueue mq) throws Exception { + return this.popOrderly(brokerAddr, mq, DEFAULT_INVISIBLE_TIME, 5000); + } + + public PopResult popOrderly(String brokerAddr, MessageQueue mq, long invisibleTime, long timeout) + throws InterruptedException, ExecutionException { + + CompletableFuture future = this.client.popMessageAsync( + brokerAddr, mq, invisibleTime, maxNum, consumerGroup, timeout, true, + ConsumeInitMode.MIN, true, ExpressionType.TAG, "*"); + + return future.get(); + } + + public CompletableFuture ackAsync(String brokerAddr, String extraInfo) { + return this.client.ackMessageAsync(brokerAddr, topic, consumerGroup, extraInfo); + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQSqlConsumer.java b/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQSqlConsumer.java new file mode 100644 index 0000000..c84843b --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQSqlConsumer.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.test.client.rmq; + +import org.apache.rocketmq.client.consumer.MessageSelector; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.listener.AbstractListener; + +public class RMQSqlConsumer extends RMQNormalConsumer { + private static Logger logger = LoggerFactory.getLogger(RMQSqlConsumer.class); + private MessageSelector selector; + + public RMQSqlConsumer(String nsAddr, String topic, MessageSelector selector, + String consumerGroup, AbstractListener listener) { + super(nsAddr, topic, "*", consumerGroup, listener); + this.selector = selector; + } + + @Override + public void create() { + super.create(); + try { + consumer.subscribe(topic, selector); + } catch (Exception e) { + logger.error("Subscribe Sql Errored", e); + } + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQTransactionalProducer.java b/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQTransactionalProducer.java new file mode 100644 index 0000000..880cfcd --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/client/rmq/RMQTransactionalProducer.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.test.client.rmq; + +import java.nio.charset.StandardCharsets; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.producer.LocalTransactionState; +import org.apache.rocketmq.client.producer.TransactionListener; +import org.apache.rocketmq.client.producer.TransactionMQProducer; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.clientinterface.AbstractMQProducer; +import org.apache.rocketmq.test.sendresult.ResultWrapper; + +public class RMQTransactionalProducer extends AbstractMQProducer { + private static Logger logger = LoggerFactory.getLogger(RMQTransactionalProducer.class); + private TransactionMQProducer producer = null; + private String nsAddr = null; + + public RMQTransactionalProducer(String nsAddr, String topic, TransactionListener transactionListener) { + this(nsAddr, topic, false, transactionListener); + } + + public RMQTransactionalProducer(String nsAddr, String topic, boolean useTLS, TransactionListener transactionListener) { + super(topic); + this.nsAddr = nsAddr; + create(useTLS, transactionListener); + start(); + } + + protected void create(boolean useTLS, TransactionListener transactionListener) { + producer = new TransactionMQProducer(); + producer.setProducerGroup(getProducerGroupName()); + producer.setInstanceName(getProducerInstanceName()); + producer.setTransactionListener(transactionListener); + producer.setUseTLS(useTLS); + + if (nsAddr != null) { + producer.setNamesrvAddr(nsAddr); + } + } + + public void start() { + try { + producer.start(); + super.setStartSuccess(true); + } catch (MQClientException e) { + super.setStartSuccess(false); + logger.error("", e); + e.printStackTrace(); + } + } + + @Override + public ResultWrapper send(Object msg, Object arg) { + boolean commitMsg = ((Pair) arg).getObject2() == LocalTransactionState.COMMIT_MESSAGE; + org.apache.rocketmq.client.producer.SendResult metaqResult = null; + Message message = (Message) msg; + try { + long start = System.currentTimeMillis(); + metaqResult = producer.sendMessageInTransaction(message, arg); + this.msgRTs.addData(System.currentTimeMillis() - start); + if (isDebug) { + logger.info("SendResult: {}", metaqResult); + } + sendResult.setMsgId(metaqResult.getMsgId()); + sendResult.setSendResult(true); + sendResult.setBrokerIp(metaqResult.getMessageQueue().getBrokerName()); + if (commitMsg) { + msgBodys.addData(new String(message.getBody(), StandardCharsets.UTF_8)); + } + originMsgs.addData(msg); + originMsgIndex.put(new String(message.getBody(), StandardCharsets.UTF_8), metaqResult); + } catch (MQClientException e) { + if (isDebug) { + e.printStackTrace(); + } + + sendResult.setSendResult(false); + sendResult.setSendException(e); + errorMsgs.addData(msg); + } + return sendResult; + } + + @Override + public void shutdown() { + producer.shutdown(); + } + +} diff --git a/test/src/main/java/org/apache/rocketmq/test/clientinterface/AbstractMQConsumer.java b/test/src/main/java/org/apache/rocketmq/test/clientinterface/AbstractMQConsumer.java new file mode 100644 index 0000000..22193bb --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/clientinterface/AbstractMQConsumer.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.test.clientinterface; + +import org.apache.rocketmq.test.listener.AbstractListener; + +public abstract class AbstractMQConsumer implements MQConsumer { + protected AbstractListener listener = null; + protected String nsAddr = null; + protected String topic = null; + protected String subExpression = null; + protected String consumerGroup = null; + protected boolean isDebug = false; + + public AbstractMQConsumer() { + } + + public AbstractMQConsumer(String nsAddr, String topic, String subExpression, + String consumerGroup, AbstractListener listener) { + this.topic = topic; + this.subExpression = subExpression; + this.consumerGroup = consumerGroup; + this.listener = listener; + this.nsAddr = nsAddr; + } + + public AbstractMQConsumer(String topic, String subExpression) { + this.topic = topic; + this.subExpression = subExpression; + } + + public void setDebug() { + if (listener != null) { + listener.setDebug(true); + } + + isDebug = true; + } + + public void setDebug(boolean isDebug) { + if (listener != null) { + listener.setDebug(isDebug); + } + + this.isDebug = isDebug; + } + + public void setSubscription(String topic, String subExpression) { + this.topic = topic; + this.subExpression = subExpression; + } + + public AbstractListener getListener() { + return listener; + } + + public void setListener(AbstractListener listener) { + this.listener = listener; + } + + public String getNsAddr() { + return nsAddr; + } + + public void setNsAddr(String nsAddr) { + this.nsAddr = nsAddr; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getSubExpression() { + return subExpression; + } + + public void setSubExpression(String subExpression) { + this.subExpression = subExpression; + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public void clearMsg() { + listener.clearMsg(); + } + +} diff --git a/test/src/main/java/org/apache/rocketmq/test/clientinterface/AbstractMQProducer.java b/test/src/main/java/org/apache/rocketmq/test/clientinterface/AbstractMQProducer.java new file mode 100644 index 0000000..258000b --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/clientinterface/AbstractMQProducer.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.test.clientinterface; + +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.List; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.sendresult.ResultWrapper; +import org.apache.rocketmq.test.util.RandomUtil; +import org.apache.rocketmq.test.util.TestUtil; + +public abstract class AbstractMQProducer extends MQCollector implements MQProducer { + protected String topic = null; + + protected ResultWrapper sendResult = new ResultWrapper(); + protected boolean startSuccess = false; + protected String producerGroupName = null; + protected String producerInstanceName = null; + protected boolean isDebug = false; + + + public AbstractMQProducer(String topic) { + super(); + producerGroupName = RandomUtil.getStringByUUID(); + producerInstanceName = RandomUtil.getStringByUUID(); + this.topic = topic; + + } + + public AbstractMQProducer(String topic, String originMsgCollector, String msgBodyCollector) { + super(originMsgCollector, msgBodyCollector); + producerGroupName = RandomUtil.getStringByUUID(); + producerInstanceName = RandomUtil.getStringByUUID(); + this.topic = topic; + } + + public boolean isStartSuccess() { + return startSuccess; + } + + public void setStartSuccess(boolean startSuccess) { + this.startSuccess = startSuccess; + } + + public String getProducerInstanceName() { + return producerInstanceName; + } + + public void setProducerInstanceName(String producerInstanceName) { + this.producerInstanceName = producerInstanceName; + } + + public String getProducerGroupName() { + return producerGroupName; + } + + public void setProducerGroupName(String producerGroupName) { + this.producerGroupName = producerGroupName; + } + + public void setDebug() { + isDebug = true; + } + + public void setDebug(boolean isDebug) { + this.isDebug = isDebug; + } + + public void setRun() { + isDebug = false; + } + + public List getMessageQueue() { + return null; + } + + private Object getMessage() { + return this.getMessageByTag(null); + } + + public Object getMessageByTag(String tag) { + Object objMsg = null; + if (this instanceof RMQNormalProducer) { + org.apache.rocketmq.common.message.Message msg = new org.apache.rocketmq.common.message.Message( + topic, (RandomUtil.getStringByUUID() + "." + new Date()).getBytes(StandardCharsets.UTF_8)); + objMsg = msg; + if (tag != null) { + msg.setTags(tag); + } + } + return objMsg; + } + + public void send() { + Object msg = getMessage(); + send(msg, null); + } + + public void send(Object msg) { + send(msg, null); + } + + public void send(long msgNum) { + for (int i = 0; i < msgNum; i++) { + this.send(); + } + } + + public void send(long msgNum, int intervalMills) { + for (int i = 0; i < msgNum; i++) { + this.send(); + TestUtil.waitForMonment(intervalMills); + } + } + + public void send(String tag, int msgSize) { + for (int i = 0; i < msgSize; i++) { + Object msg = getMessageByTag(tag); + send(msg, null); + } + } + + public void send(String tag, int msgSize, int intervalMills) { + for (int i = 0; i < msgSize; i++) { + Object msg = getMessageByTag(tag); + send(msg, null); + TestUtil.waitForMonment(intervalMills); + } + } + + public void send(List msgs) { + for (Object msg : msgs) { + this.send(msg, null); + } + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/clientinterface/MQCollector.java b/test/src/main/java/org/apache/rocketmq/test/clientinterface/MQCollector.java new file mode 100644 index 0000000..7ccf92a --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/clientinterface/MQCollector.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.test.clientinterface; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.test.util.RandomUtil; +import org.apache.rocketmq.test.util.data.collect.DataCollector; +import org.apache.rocketmq.test.util.data.collect.DataCollectorManager; + +public abstract class MQCollector { + protected DataCollector msgBodys = null; + protected DataCollector originMsgs = null; + protected DataCollector errorMsgs = null; + protected Map originMsgIndex = null; + protected Collection msgBodysCopy = null; + + protected DataCollector msgRTs = null; + + public MQCollector() { + msgBodys = DataCollectorManager.getInstance() + .fetchListDataCollector(RandomUtil.getStringByUUID()); + originMsgs = DataCollectorManager.getInstance() + .fetchListDataCollector(RandomUtil.getStringByUUID()); + errorMsgs = DataCollectorManager.getInstance() + .fetchListDataCollector(RandomUtil.getStringByUUID()); + originMsgIndex = new ConcurrentHashMap(); + msgRTs = DataCollectorManager.getInstance() + .fetchListDataCollector(RandomUtil.getStringByUUID()); + } + + public MQCollector(String originMsgCollector, String msgBodyCollector) { + originMsgs = DataCollectorManager.getInstance().fetchDataCollector(originMsgCollector); + msgBodys = DataCollectorManager.getInstance().fetchDataCollector(msgBodyCollector); + } + + public Collection getAllMsgBody() { + return msgBodys.getAllData(); + } + + public Collection getAllOriginMsg() { + return originMsgs.getAllData(); + } + + public Object getFirstMsg() { + return ((List) originMsgs.getAllData()).get(0); + } + + public Collection getAllUndupMsgBody() { + return msgBodys.getAllDataWithoutDuplicate(); + } + + public Collection getAllUndupOriginMsg() { + return originMsgs.getAllData(); + } + + public Collection getSendErrorMsg() { + return errorMsgs.getAllData(); + } + + public Collection getMsgRTs() { + return msgRTs.getAllData(); + } + + public Map getOriginMsgIndex() { + return originMsgIndex; + } + + public Collection getMsgBodysCopy() { + msgBodysCopy = new ArrayList(); + msgBodysCopy.addAll(msgBodys.getAllData()); + return msgBodysCopy; + } + + public void clearMsg() { + if (msgBodys != null) { + msgBodys.resetData(); + } + if (originMsgs != null) { + originMsgs.resetData(); + } + if (originMsgs != null) { + errorMsgs.resetData(); + } + if (originMsgIndex != null) { + originMsgIndex.clear(); + } + if (msgRTs != null) { + msgRTs.resetData(); + } + } + + public void lockCollectors() { + msgBodys.lockIncrement(); + originMsgs.lockIncrement(); + errorMsgs.lockIncrement(); + msgRTs.lockIncrement(); + + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/clientinterface/MQConsumer.java b/test/src/main/java/org/apache/rocketmq/test/clientinterface/MQConsumer.java new file mode 100644 index 0000000..0fc2e96 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/clientinterface/MQConsumer.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.test.clientinterface; + +public interface MQConsumer { + void create(); + + void create(boolean useTLS); + + void start(); + + void shutdown(); +} diff --git a/test/src/main/java/org/apache/rocketmq/test/clientinterface/MQProducer.java b/test/src/main/java/org/apache/rocketmq/test/clientinterface/MQProducer.java new file mode 100644 index 0000000..e9ed0d3 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/clientinterface/MQProducer.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.test.clientinterface; + +import org.apache.rocketmq.test.sendresult.ResultWrapper; + +public interface MQProducer { + ResultWrapper send(Object msg, Object arg); + + void setDebug(); + + void setRun(); + + void shutdown(); +} diff --git a/test/src/main/java/org/apache/rocketmq/test/factory/ConsumerFactory.java b/test/src/main/java/org/apache/rocketmq/test/factory/ConsumerFactory.java new file mode 100644 index 0000000..cdda908 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/factory/ConsumerFactory.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.test.factory; + +import java.util.UUID; +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.client.consumer.MessageSelector; +import org.apache.rocketmq.test.client.rmq.RMQBroadCastConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQPopClient; +import org.apache.rocketmq.test.client.rmq.RMQPopConsumer; +import org.apache.rocketmq.test.client.rmq.RMQSqlConsumer; +import org.apache.rocketmq.test.listener.AbstractListener; + +public class ConsumerFactory { + + public static RMQNormalConsumer getRMQNormalConsumer(String nsAddr, String consumerGroup, + String topic, String subExpression, + AbstractListener listener) { + return getRMQNormalConsumer(nsAddr, consumerGroup, topic, subExpression, listener, false); + } + + public static RMQNormalConsumer getRMQNormalConsumer(String nsAddr, String consumerGroup, + String topic, String subExpression, + AbstractListener listener, boolean useTLS) { + RMQNormalConsumer consumer = new RMQNormalConsumer(nsAddr, topic, subExpression, + consumerGroup, listener); + consumer.create(useTLS); + consumer.start(); + return consumer; + } + + public static RMQBroadCastConsumer getRMQBroadCastConsumer(String nsAddr, String consumerGroup, + String topic, String subExpression, + AbstractListener listner) { + RMQBroadCastConsumer consumer = new RMQBroadCastConsumer(nsAddr, topic, subExpression, + consumerGroup, listner); + consumer.create(); + consumer.start(); + return consumer; + } + + public static RMQSqlConsumer getRMQSqlConsumer(String nsAddr, String consumerGroup, + String topic, MessageSelector selector, + AbstractListener listner) { + RMQSqlConsumer consumer = new RMQSqlConsumer(nsAddr, topic, selector, + consumerGroup, listner); + consumer.create(); + consumer.start(); + return consumer; + } + + public static RMQPopConsumer getRMQPopConsumer(String nsAddr, String consumerGroup, + String topic, String subExpression, AbstractListener listener) { + + RMQPopConsumer consumer = new RMQPopConsumer(nsAddr, topic, subExpression, consumerGroup, listener); + consumer.create(); + consumer.start(); + return consumer; + } + + public static RMQPopClient getRMQPopClient() { + RMQPopClient client = new RMQPopClient(); + client.create(); + client.start(); + return client; + } + + public static DefaultMQPullConsumer getRMQPullConsumer(String nsAddr, String consumerGroup) throws Exception { + DefaultMQPullConsumer defaultMQPullConsumer = new DefaultMQPullConsumer(consumerGroup); + defaultMQPullConsumer.setInstanceName(UUID.randomUUID().toString()); + defaultMQPullConsumer.setNamesrvAddr(nsAddr); + defaultMQPullConsumer.start(); + return defaultMQPullConsumer; + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/factory/MQMessageFactory.java b/test/src/main/java/org/apache/rocketmq/test/factory/MQMessageFactory.java new file mode 100644 index 0000000..ca472f0 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/factory/MQMessageFactory.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.test.factory; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.test.util.RandomUtil; + +public class MQMessageFactory { + private static Integer index = 0; + + public static List getRMQMessage(String tag, String topic, int msgSize) { + List msgs = new ArrayList(); + for (int i = 0; i < msgSize; i++) { + msgs.add(new Message(topic, tag, RandomUtil.getStringByUUID().getBytes(StandardCharsets.UTF_8))); + } + + return msgs; + } + + public static List getRMQMessage(List tags, String topic, int msgSize) { + List msgs = new ArrayList(); + for (int i = 0; i < msgSize; i++) { + for (String tag : tags) { + msgs.add(new Message(topic, tag, RandomUtil.getStringByUUID().getBytes(StandardCharsets.UTF_8))); + } + } + return msgs; + } + + public static List getMessageBody(List msgs) { + List msgBodys = new ArrayList(); + for (Object msg : msgs) { + msgBodys.add(new String(((Message) msg).getBody(), StandardCharsets.UTF_8)); + } + + return msgBodys; + } + + public static Collection getMessage(Collection... msgs) { + Collection allMsgs = new ArrayList(); + for (Collection msg : msgs) { + allMsgs.addAll(msg); + } + return allMsgs; + } + + public static List getDelayMsg(String topic, int delayLevel, int msgSize) { + List msgs = new ArrayList(); + for (int i = 0; i < msgSize; i++) { + Message msg = new Message(topic, RandomUtil.getStringByUUID().getBytes(StandardCharsets.UTF_8)); + msg.setDelayTimeLevel(delayLevel); + msgs.add(msg); + } + return msgs; + } + + public static List getKeyMsg(String topic, String key, int msgSize) { + List msgs = new ArrayList(); + for (int i = 0; i < msgSize; i++) { + Message msg = new Message(topic, null, key, RandomUtil.getStringByUUID().getBytes(StandardCharsets.UTF_8)); + msgs.add(msg); + } + return msgs; + } + + public static Map> getMsgByMQ(MessageQueue mq, int msgSize) { + List mqs = new ArrayList(); + mqs.add(mq); + return getMsgByMQ(mqs, msgSize); + } + + public static Map> getMsgByMQ(List mqs, int msgSize) { + return getMsgByMQ(mqs, msgSize, null); + } + + public static Map> getMsgByMQ(List mqs, int msgSize, + String tag) { + Map> msgs = new HashMap>(); + for (MessageQueue mq : mqs) { + msgs.put(mq, getMsg(mq.getTopic(), msgSize, tag)); + } + return msgs; + } + + public static List getMsg(String topic, int msgSize) { + return getMsg(topic, msgSize, null); + } + + public static List getMsg(String topic, int msgSize, String tag) { + List msgs = new ArrayList(); + while (msgSize > 0) { + Message msg = new Message(topic, (index++).toString().getBytes(StandardCharsets.UTF_8)); + if (tag != null) { + msg.setTags(tag); + } + msgs.add(msg); + msgSize--; + } + + return msgs; + } + + public static List getMessageQueues(MessageQueue... mq) { + return Arrays.asList(mq); + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/factory/MessageFactory.java b/test/src/main/java/org/apache/rocketmq/test/factory/MessageFactory.java new file mode 100644 index 0000000..f5b5428 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/factory/MessageFactory.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.test.factory; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.test.util.RandomUtils; + +public class MessageFactory { + + public static Message getRandomMessage(String topic) { + return getStringMessage(topic, RandomUtils.getStringByUUID()); + } + + public static Message getStringMessage(String topic, String body) { + return new Message(topic, body.getBytes(StandardCharsets.UTF_8)); + } + + public static Message getStringMessageByTag(String topic, String tags, String body) { + return new Message(topic, tags, body.getBytes(StandardCharsets.UTF_8)); + } + + public static Message getRandomMessageByTag(String topic, String tags) { + return getStringMessageByTag(topic, tags, RandomUtils.getStringByUUID()); + } + + public static Collection getRandomMessageList(String topic, int size) { + List msgList = new ArrayList(); + for (int i = 0; i < size; i++) { + msgList.add(getRandomMessage(topic)); + } + return msgList; + } + + public static Collection getRandomMessageListByTag(String topic, String tags, int size) { + List msgList = new ArrayList(); + for (int i = 0; i < size; i++) { + msgList.add(getRandomMessageByTag(topic, tags)); + } + return msgList; + } + +} diff --git a/test/src/main/java/org/apache/rocketmq/test/factory/ProducerFactory.java b/test/src/main/java/org/apache/rocketmq/test/factory/ProducerFactory.java new file mode 100644 index 0000000..76e6e09 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/factory/ProducerFactory.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.test.factory; + +import java.util.UUID; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.test.util.RandomUtil; + +public class ProducerFactory { + + public static DefaultMQProducer getRMQProducer(String ns) { + DefaultMQProducer producer = new DefaultMQProducer(RandomUtil.getStringByUUID()); + producer.setInstanceName(UUID.randomUUID().toString()); + producer.setNamesrvAddr(ns); + try { + producer.start(); + } catch (MQClientException e) { + e.printStackTrace(); + } + + return producer; + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/factory/SendCallBackFactory.java b/test/src/main/java/org/apache/rocketmq/test/factory/SendCallBackFactory.java new file mode 100644 index 0000000..64764c6 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/factory/SendCallBackFactory.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.test.factory; + +import org.apache.rocketmq.client.producer.SendCallback; +import org.apache.rocketmq.client.producer.SendResult; + +public class SendCallBackFactory { + public static SendCallback getSendCallBack() { + return new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + } + + @Override + public void onException(Throwable throwable) { + } + }; + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/factory/TagMessage.java b/test/src/main/java/org/apache/rocketmq/test/factory/TagMessage.java new file mode 100644 index 0000000..b7eb6a6 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/factory/TagMessage.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.test.factory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TagMessage { + private List tags = null; + private String topic = null; + private int msgSize = 0; + private Map> rmqMsgs = new HashMap>(); + + public TagMessage(String tag, String topic, int msgSize) { + String[] tags = {tag}; + this.tags = Arrays.asList(tags); + this.topic = topic; + this.msgSize = msgSize; + + init(); + } + + public TagMessage(String[] tags, String topic, int msgSize) { + this(Arrays.asList(tags), topic, msgSize); + } + + public TagMessage(List tags, String topic, int msgSize) { + this.tags = tags; + this.topic = topic; + this.msgSize = msgSize; + + init(); + } + + private void init() { + for (String tag : tags) { + List tagMsgs = MQMessageFactory.getRMQMessage(tag, topic, msgSize); + rmqMsgs.put(tag, tagMsgs); + } + } + + public List getMessageByTag(String tag) { + if (tags.contains(tag)) { + return rmqMsgs.get(tag); + } else { + return new ArrayList(); + } + } + + public List getMixedTagMessages() { + List mixedMsgs = new ArrayList(); + for (int i = 0; i < msgSize; i++) { + for (String tag : tags) { + mixedMsgs.add(rmqMsgs.get(tag).get(i)); + } + } + + return mixedMsgs; + } + + public List getMessageBodyByTag(String tag) { + if (tags.contains(tag)) { + return MQMessageFactory.getMessageBody(rmqMsgs.get(tag)); + } else { + return new ArrayList(); + } + } + + public List getMessageBodyByTag(String... tag) { + return this.getMessageBodyByTag(Arrays.asList(tag)); + } + + public List getMessageBodyByTag(List tags) { + List msgBodys = new ArrayList(); + for (String tag : tags) { + msgBodys.addAll(MQMessageFactory.getMessageBody(rmqMsgs.get(tag))); + } + return msgBodys; + } + + public List getAllTagMessageBody() { + List msgs = new ArrayList(); + for (String tag : tags) { + msgs.addAll(MQMessageFactory.getMessageBody(rmqMsgs.get(tag))); + } + + return msgs; + } + +} diff --git a/test/src/main/java/org/apache/rocketmq/test/listener/AbstractListener.java b/test/src/main/java/org/apache/rocketmq/test/listener/AbstractListener.java new file mode 100644 index 0000000..4604380 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/listener/AbstractListener.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.test.listener; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import org.apache.rocketmq.client.consumer.listener.MessageListener; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.clientinterface.MQCollector; +import org.apache.rocketmq.test.util.TestUtil; + +public class AbstractListener extends MQCollector implements MessageListener { + public static final Logger LOGGER = LoggerFactory.getLogger(AbstractListener.class); + protected boolean isDebug = true; + protected String listenerName = null; + protected Collection allSendMsgs = null; + + public AbstractListener() { + super(); + } + + public AbstractListener(String listenerName) { + super(); + this.listenerName = listenerName; + } + + public AbstractListener(String originMsgCollector, String msgBodyCollector) { + super(originMsgCollector, msgBodyCollector); + } + + public boolean isDebug() { + return isDebug; + } + + public void setDebug(boolean debug) { + isDebug = debug; + } + + public void waitForMessageConsume(int timeoutMills) { + TestUtil.waitForMonment(timeoutMills); + } + + public void stopRecv() { + super.lockCollectors(); + } + + public Collection waitForMessageConsume(Collection allSendMessages, int timeoutMills) { + this.allSendMsgs = allSendMessages; + List sendMessages = new ArrayList<>(allSendMessages); + + long curTime = System.currentTimeMillis(); + while (!sendMessages.isEmpty()) { + sendMessages.removeIf(msg -> msgBodys.getAllData().contains(msg)); + if (sendMessages.isEmpty()) { + break; + } else { + if (System.currentTimeMillis() - curTime >= timeoutMills) { + LOGGER.error(String.format("timeout but [%s] not recv all send messages!", listenerName)); + break; + } else { + LOGGER.info(String.format("[%s] still [%s] msg not recv!", listenerName, sendMessages.size())); + TestUtil.waitForMonment(500); + } + } + } + return sendMessages; + } + + public long waitForMessageConsume(int size, int timeoutMills) { + long curTime = System.currentTimeMillis(); + while (true) { + if (msgBodys.getDataSize() >= size) { + break; + } + if (System.currentTimeMillis() - curTime >= timeoutMills) { + LOGGER.error(String.format("timeout but [%s] not recv all send messages!", listenerName)); + break; + } else { + LOGGER.info(String.format("[%s] still [%s] msg not recv!", + listenerName, size - msgBodys.getDataSize())); + TestUtil.waitForMonment(500); + } + } + + return msgBodys.getDataSize(); + } + + public void waitForMessageConsume(Map sendMsgIndex, int timeoutMills) { + Collection notRecvMsgs = waitForMessageConsume(sendMsgIndex.keySet(), timeoutMills); + for (Object object : notRecvMsgs) { + LOGGER.info("{}", sendMsgIndex.get(object)); + } + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/listener/rmq/concurrent/RMQBlockListener.java b/test/src/main/java/org/apache/rocketmq/test/listener/rmq/concurrent/RMQBlockListener.java new file mode 100644 index 0000000..907612c --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/listener/rmq/concurrent/RMQBlockListener.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.test.listener.rmq.concurrent; + +import java.util.List; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.common.message.MessageExt; + +public class RMQBlockListener extends RMQNormalListener { + private volatile boolean block = true; + private volatile boolean inBlock = true; + + public RMQBlockListener() { + super(); + } + + public RMQBlockListener(boolean block) { + super(); + this.block = block; + } + + public boolean isBlocked() { + return inBlock; + } + + public void setBlock(boolean block) { + this.block = block; + } + + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + ConsumeConcurrentlyStatus status = super.consumeMessage(msgs, context); + + try { + while (block) { + inBlock = true; + Thread.sleep(100); + } + } catch (InterruptedException ignore) { + } + + return status; + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/listener/rmq/concurrent/RMQDelayListener.java b/test/src/main/java/org/apache/rocketmq/test/listener/rmq/concurrent/RMQDelayListener.java new file mode 100644 index 0000000..e414d1f --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/listener/rmq/concurrent/RMQDelayListener.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.test.listener.rmq.concurrent; + +import java.nio.charset.StandardCharsets; +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 org.apache.rocketmq.test.listener.AbstractListener; +import org.apache.rocketmq.test.util.RandomUtil; +import org.apache.rocketmq.test.util.data.collect.DataCollector; +import org.apache.rocketmq.test.util.data.collect.DataCollectorManager; + +import java.util.Collection; +import java.util.List; + +public class RMQDelayListener extends AbstractListener implements MessageListenerConcurrently { + private DataCollector msgDelayTimes = null; + + public RMQDelayListener() { + msgDelayTimes = DataCollectorManager.getInstance() + .fetchDataCollector(RandomUtil.getStringByUUID()); + } + + public Collection getMsgDelayTimes() { + return msgDelayTimes.getAllData(); + } + + public void resetMsgDelayTimes() { + msgDelayTimes.resetData(); + } + + public ConsumeConcurrentlyStatus consumeMessage(List msgs, + ConsumeConcurrentlyContext consumeConcurrentlyContext) { + long recvTime = System.currentTimeMillis(); + for (MessageExt msg : msgs) { + if (isDebug) { + LOGGER.info(listenerName + ":" + msg); + } + + msgBodys.addData(new String(msg.getBody(), StandardCharsets.UTF_8)); + originMsgs.addData(msg); + msgDelayTimes.addData(Math.abs(recvTime - msg.getBornTimestamp())); + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/listener/rmq/concurrent/RMQNormalListener.java b/test/src/main/java/org/apache/rocketmq/test/listener/rmq/concurrent/RMQNormalListener.java new file mode 100644 index 0000000..1a0345b --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/listener/rmq/concurrent/RMQNormalListener.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.test.listener.rmq.concurrent; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +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 org.apache.rocketmq.test.listener.AbstractListener; + +public class RMQNormalListener extends AbstractListener implements MessageListenerConcurrently { + + private ConsumeConcurrentlyStatus consumeStatus = ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + + private final AtomicInteger msgIndex = new AtomicInteger(0); + + public RMQNormalListener() { + super(); + } + + public RMQNormalListener(String listenerName) { + super(listenerName); + } + + public RMQNormalListener(ConsumeConcurrentlyStatus consumeStatus) { + super(); + this.consumeStatus = consumeStatus; + } + + public RMQNormalListener(String originMsgCollector, String msgBodyCollector) { + super(originMsgCollector, msgBodyCollector); + } + + public AtomicInteger getMsgIndex() { + return msgIndex; + } + + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, + ConsumeConcurrentlyContext consumeConcurrentlyContext) { + for (MessageExt msg : msgs) { + msgIndex.getAndIncrement(); + if (isDebug) { + if (listenerName != null && !listenerName.isEmpty()) { + LOGGER.info(listenerName + ":" + msgIndex.get() + ":" + + String.format("msgid:%s broker:%s queueId:%s offset:%s", + msg.getMsgId(), msg.getStoreHost(), msg.getQueueId(), + msg.getQueueOffset())); + } else { + LOGGER.info("{}", msg); + } + } + + msgBodys.addData(new String(msg.getBody(), StandardCharsets.UTF_8)); + originMsgs.addData(msg); + if (originMsgIndex != null) { + originMsgIndex.put(new String(msg.getBody(), StandardCharsets.UTF_8), msg); + } + } + return consumeStatus; + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/listener/rmq/order/RMQOrderListener.java b/test/src/main/java/org/apache/rocketmq/test/listener/rmq/order/RMQOrderListener.java new file mode 100644 index 0000000..85e249e --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/listener/rmq/order/RMQOrderListener.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.test.listener.rmq.order; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +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.message.MessageExt; +import org.apache.rocketmq.test.listener.AbstractListener; + +public class RMQOrderListener extends AbstractListener implements MessageListenerOrderly { + private Map> msgs = new ConcurrentHashMap>(); + + public RMQOrderListener() { + super(); + } + + public RMQOrderListener(String listnerName) { + super(listnerName); + } + + public RMQOrderListener(String originMsgCollector, String msgBodyCollector) { + super(originMsgCollector, msgBodyCollector); + } + + public Collection> getMsgs() { + return msgs.values(); + } + + private void putMsg(MessageExt msg) { + Collection msgQueue = null; + String key = getKey(msg.getQueueId(), msg.getStoreHost().toString()); + if (!msgs.containsKey(key)) { + msgQueue = new ArrayList(); + } else { + msgQueue = msgs.get(key); + } + + msgQueue.add(new String(msg.getBody(), StandardCharsets.UTF_8)); + msgs.put(key, msgQueue); + } + + private String getKey(int queueId, String brokerIp) { + return String.format("%s_%s", queueId, brokerIp); + } + + @Override + public ConsumeOrderlyStatus consumeMessage(List msgs, + ConsumeOrderlyContext context) { + for (MessageExt msg : msgs) { + if (isDebug) { + if (listenerName != null && listenerName != "") { + LOGGER.info(listenerName + ": " + msg); + } else { + LOGGER.info("{}", msg); + } + } + + putMsg(msg); + msgBodys.addData(new String(msg.getBody(), StandardCharsets.UTF_8)); + originMsgs.addData(msg); + } + + return ConsumeOrderlyStatus.SUCCESS; + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/lmq/benchmark/BenchLmqStore.java b/test/src/main/java/org/apache/rocketmq/test/lmq/benchmark/BenchLmqStore.java new file mode 100644 index 0000000..2351e84 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/lmq/benchmark/BenchLmqStore.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.test.lmq.benchmark; + +import com.google.common.math.IntMath; +import com.google.common.math.LongMath; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.commons.lang3.StringUtils; +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.client.consumer.PullStatus; +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.common.MixAll; +import org.apache.rocketmq.common.ThreadFactoryImpl; +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.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateConsumerOffsetRequestHeader; +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 org.apache.rocketmq.test.util.StatUtil; + +public class BenchLmqStore { + private static Logger logger = LoggerFactory.getLogger(BenchLmqStore.class); + private static String namesrv = System.getProperty("namesrv", "127.0.0.1:9876"); + private static String lmqTopic = System.getProperty("lmqTopic", "lmqTestTopic"); + private static boolean enableSub = Boolean.parseBoolean(System.getProperty("enableSub", "true")); + private static String queuePrefix = System.getProperty("queuePrefix", "lmqTest"); + private static int tps = Integer.parseInt(System.getProperty("tps", "1")); + private static int lmqNum = Integer.parseInt(System.getProperty("lmqNum", "1")); + private static int sendThreadNum = Integer.parseInt(System.getProperty("sendThreadNum", "64")); + private static int consumerThreadNum = Integer.parseInt(System.getProperty("consumerThreadNum", "64")); + private static String brokerName = System.getProperty("brokerName", "broker-a"); + private static int size = Integer.parseInt(System.getProperty("size", "128")); + private static int suspendTime = Integer.parseInt(System.getProperty("suspendTime", "2000")); + private static final boolean RETRY_NO_MATCHED_MSG = Boolean.parseBoolean(System.getProperty("retry_no_matched_msg", "false")); + private static boolean benchOffset = Boolean.parseBoolean(System.getProperty("benchOffset", "false")); + private static int benchOffsetNum = Integer.parseInt(System.getProperty("benchOffsetNum", "1")); + private static Map offsetMap = new ConcurrentHashMap<>(256); + private static Map pullStatus = new ConcurrentHashMap<>(256); + private static Map> pullEvent = new ConcurrentHashMap<>(256); + public static DefaultMQProducer defaultMQProducer; + private static int pullConsumerNum = Integer.parseInt(System.getProperty("pullConsumerNum", "8")); + public static DefaultMQPullConsumer[] defaultMQPullConsumers = new DefaultMQPullConsumer[pullConsumerNum]; + private static AtomicLong rid = new AtomicLong(); + private static final String LMQ_PREFIX = "%LMQ%"; + + public static void main(String[] args) throws InterruptedException, MQClientException, MQBrokerException, + RemotingException { + defaultMQProducer = new DefaultMQProducer(); + defaultMQProducer.setProducerGroup("PID_LMQ_TEST"); + defaultMQProducer.setVipChannelEnabled(false); + defaultMQProducer.setNamesrvAddr(namesrv); + defaultMQProducer.start(); + //defaultMQProducer.createTopic(lmqTopic, lmqTopic, 8); + for (int i = 0; i < pullConsumerNum; i++) { + DefaultMQPullConsumer defaultMQPullConsumer = new DefaultMQPullConsumer(); + defaultMQPullConsumers[i] = defaultMQPullConsumer; + defaultMQPullConsumer.setNamesrvAddr(namesrv); + defaultMQPullConsumer.setVipChannelEnabled(false); + defaultMQPullConsumer.setConsumerGroup("CID_RMQ_SYS_LMQ_TEST_" + i); + defaultMQPullConsumer.setInstanceName("CID_RMQ_SYS_LMQ_TEST_" + i); + defaultMQPullConsumer.setRegisterTopics(new HashSet<>(Collections.singletonList(lmqTopic))); + defaultMQPullConsumer.setBrokerSuspendMaxTimeMillis(suspendTime); + defaultMQPullConsumer.setConsumerTimeoutMillisWhenSuspend(suspendTime + 1000); + defaultMQPullConsumer.start(); + } + Thread.sleep(3000L); + if (benchOffset) { + doBenchOffset(); + return; + } + ScheduledThreadPoolExecutor consumerPool = new ScheduledThreadPoolExecutor(consumerThreadNum, new ThreadFactoryImpl("test")); + for (int i = 0; i < consumerThreadNum; i++) { + final int idx = i; + consumerPool.scheduleWithFixedDelay(() -> { + try { + Map map = pullEvent.get(idx); + if (map == null) { + return; + } + for (Map.Entry entry : map.entrySet()) { + try { + Boolean status = pullStatus.get(entry.getKey()); + if (Boolean.TRUE.equals(status)) { + continue; + } + doPull(map, entry.getKey(), entry.getValue()); + } catch (Exception e) { + logger.error("pull broker msg error", e); + } + } + } catch (Exception e) { + logger.error("exec doPull task error", e); + } + }, 1, 1, TimeUnit.MILLISECONDS); + } + // init queue sub + if (enableSub && lmqNum > 0 && StringUtils.isNotBlank(brokerName)) { + for (int i = 0; i < lmqNum; i++) { + long idx = rid.incrementAndGet(); + String queue = LMQ_PREFIX + queuePrefix + LongMath.mod(idx, lmqNum); + MessageQueue mq = new MessageQueue(queue, brokerName, 0); + int queueHash = IntMath.mod(queue.hashCode(), consumerThreadNum); + pullEvent.putIfAbsent(queueHash, new ConcurrentHashMap<>()); + pullEvent.get(queueHash).put(mq, idx); + } + } + Thread.sleep(5000L); + doSend(); + } + + public static void doSend() { + StringBuilder sb = new StringBuilder(); + for (int j = 0; j < size; j += 10) { + sb.append("hello baby"); + } + byte[] body = sb.toString().getBytes(StandardCharsets.UTF_8); + String pubKey = "pub"; + ExecutorService sendPool = Executors.newFixedThreadPool(sendThreadNum); + for (int i = 0; i < sendThreadNum; i++) { + sendPool.execute(() -> { + while (true) { + if (StatUtil.isOverFlow(pubKey, tps)) { + try { + Thread.sleep(100L); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + long start = System.currentTimeMillis(); + try { + long idx = rid.incrementAndGet(); + Message message = new Message(lmqTopic, body); + String queue = lmqTopic; + if (lmqNum > 0) { + queue = LMQ_PREFIX + queuePrefix + idx % lmqNum; + message.putUserProperty("INNER_MULTI_DISPATCH", queue); + } + SendResult sendResult = defaultMQProducer.send(message); + StatUtil.addInvoke(pubKey, System.currentTimeMillis() - start); + if (StatUtil.nowTps(pubKey) < 10) { + logger.warn("pub: {} ", sendResult.getMsgId()); + } + if (enableSub && null != sendResult.getMessageQueue()) { + MessageQueue mq = new MessageQueue(queue, sendResult.getMessageQueue().getBrokerName(), + lmqNum > 0 ? 0 : sendResult.getMessageQueue().getQueueId()); + int queueHash = IntMath.mod(queue.hashCode(), consumerThreadNum); + pullEvent.putIfAbsent(queueHash, new ConcurrentHashMap<>()); + pullEvent.get(queueHash).put(mq, idx); + } + } catch (Exception e) { + logger.error("", e); + StatUtil.addInvoke(pubKey, System.currentTimeMillis() - start, false); + } + } + }); + } + } + + public static void doPull(Map eventMap, MessageQueue mq, + Long eventId) throws RemotingException, InterruptedException, MQClientException { + if (!enableSub) { + eventMap.remove(mq, eventId); + pullStatus.remove(mq); + return; + } + DefaultMQPullConsumer defaultMQPullConsumer = defaultMQPullConsumers[(int) (eventId % pullConsumerNum)]; + Long offset = offsetMap.get(mq); + if (offset == null) { + long start = System.currentTimeMillis(); + offset = defaultMQPullConsumer.maxOffset(mq); + StatUtil.addInvoke("maxOffset", System.currentTimeMillis() - start); + offsetMap.put(mq, offset); + } + long start = System.currentTimeMillis(); + if (null != pullStatus.putIfAbsent(mq, true)) { + return; + } + defaultMQPullConsumer.pullBlockIfNotFound( + mq, "*", offset, 32, + new PullCallback() { + @Override + public void onSuccess(PullResult pullResult) { + StatUtil.addInvoke(pullResult.getPullStatus().name(), System.currentTimeMillis() - start); + eventMap.remove(mq, eventId); + pullStatus.remove(mq); + offsetMap.put(mq, pullResult.getNextBeginOffset()); + StatUtil.addInvoke("doPull", System.currentTimeMillis() - start); + if (PullStatus.NO_MATCHED_MSG.equals(pullResult.getPullStatus()) && RETRY_NO_MATCHED_MSG) { + long idx = rid.incrementAndGet(); + eventMap.put(mq, idx); + } + List list = pullResult.getMsgFoundList(); + if (list == null || list.isEmpty()) { + StatUtil.addInvoke("NoMsg", System.currentTimeMillis() - start); + return; + } + for (MessageExt messageExt : list) { + StatUtil.addInvoke("sub", System.currentTimeMillis() - messageExt.getBornTimestamp()); + if (StatUtil.nowTps("sub") < 10) { + logger.warn("sub: {}", messageExt.getMsgId()); + } + } + } + + @Override + public void onException(Throwable e) { + eventMap.remove(mq, eventId); + pullStatus.remove(mq); + logger.error("", e); + StatUtil.addInvoke("doPull", System.currentTimeMillis() - start, false); + } + }); + } + + public static void doBenchOffset() throws RemotingException, InterruptedException, MQClientException { + ExecutorService sendPool = Executors.newFixedThreadPool(sendThreadNum); + Map offsetMap = new ConcurrentHashMap<>(); + String statKey = "benchOffset"; + TopicRouteData topicRouteData = defaultMQPullConsumers[0].getDefaultMQPullConsumerImpl(). + getRebalanceImpl().getmQClientFactory().getMQClientAPIImpl(). + getTopicRouteInfoFromNameServer(lmqTopic, 3000); + HashMap brokerMap = topicRouteData.getBrokerDatas().get(0).getBrokerAddrs(); + if (brokerMap == null || brokerMap.isEmpty()) { + return; + } + String brokerAddress = brokerMap.get(MixAll.MASTER_ID); + for (int i = 0; i < sendThreadNum; i++) { + final int flag = i; + sendPool.execute(new Runnable() { + @Override + public void run() { + while (true) { + try { + if (StatUtil.isOverFlow(statKey, tps)) { + Thread.sleep(100L); + } + long start = System.currentTimeMillis(); + long id = rid.incrementAndGet(); + int index = (Integer.MAX_VALUE & (int) id) % defaultMQPullConsumers.length; + DefaultMQPullConsumer defaultMQPullConsumer = defaultMQPullConsumers[index]; + String lmq = LMQ_PREFIX + queuePrefix + id % benchOffsetNum; + String lmqCid = LMQ_PREFIX + "GID_LMQ@@c" + flag + "-" + id % benchOffsetNum; + offsetMap.putIfAbsent(lmq, 0L); + long newOffset1 = offsetMap.get(lmq) + 1; + UpdateConsumerOffsetRequestHeader updateHeader = new UpdateConsumerOffsetRequestHeader(); + updateHeader.setTopic(lmq); + updateHeader.setConsumerGroup(lmqCid); + updateHeader.setQueueId(0); + updateHeader.setCommitOffset(newOffset1); + defaultMQPullConsumer + .getDefaultMQPullConsumerImpl() + .getRebalanceImpl() + .getmQClientFactory() + .getMQClientAPIImpl().updateConsumerOffset(brokerAddress, updateHeader, 1000); + QueryConsumerOffsetRequestHeader queryHeader = new QueryConsumerOffsetRequestHeader(); + queryHeader.setTopic(lmq); + queryHeader.setConsumerGroup(lmqCid); + queryHeader.setQueueId(0); + long newOffset2 = defaultMQPullConsumer + .getDefaultMQPullConsumerImpl() + .getRebalanceImpl() + .getmQClientFactory() + .getMQClientAPIImpl() + .queryConsumerOffset(brokerAddress, queryHeader, 1000); + offsetMap.put(lmq, newOffset2); + if (newOffset1 != newOffset2) { + StatUtil.addInvoke("ErrorOffset", 1); + } + StatUtil.addInvoke(statKey, System.currentTimeMillis() - start); + } catch (Exception e) { + logger.error("", e); + } + } + } + }); + } + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/message/MessageQueueMsg.java b/test/src/main/java/org/apache/rocketmq/test/message/MessageQueueMsg.java new file mode 100644 index 0000000..b500bd5 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/message/MessageQueueMsg.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.test.message; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.test.factory.MQMessageFactory; + +public class MessageQueueMsg { + private Map> msgsWithMQ = null; + private Map> msgsWithMQId = null; + private Collection msgBodys = null; + + public MessageQueueMsg(List mqs, int msgSize) { + this(mqs, msgSize, null); + } + + public MessageQueueMsg(List mqs, int msgSize, String tag) { + msgsWithMQ = MQMessageFactory.getMsgByMQ(mqs, msgSize, tag); + msgsWithMQId = new HashMap>(); + msgBodys = new ArrayList(); + init(); + } + + public Map> getMsgsWithMQ() { + return msgsWithMQ; + } + + public Map> getMsgWithMQId() { + return msgsWithMQId; + } + + public Collection getMsgBodys() { + return msgBodys; + } + + private void init() { + for (Entry> mqEntry : msgsWithMQ.entrySet()) { + msgsWithMQId.put(mqEntry.getKey().getQueueId(), mqEntry.getValue()); + msgBodys.addAll(MQMessageFactory.getMessageBody(mqEntry.getValue())); + } + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/schema/SchemaDefiner.java b/test/src/main/java/org/apache/rocketmq/test/schema/SchemaDefiner.java new file mode 100644 index 0000000..96ad5ac --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/schema/SchemaDefiner.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.test.schema; + +import com.google.common.collect.Sets; +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.client.ClientConfig; +import org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy; +import org.apache.rocketmq.client.consumer.DefaultLitePullConsumer; +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +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.ConsumeOrderlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListener; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; +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.client.producer.SendStatus; +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.CommandCustomHeader; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.reflections.Reflections; + +public class SchemaDefiner { + public static final Map, Set> IGNORED_FIELDS = new HashMap<>(); + //Use name as the key instead of X.class directly. X.class is not equal to field.getType(). + public static final Set FIELD_CLASS_NAMES = new HashSet<>(); + public static final List> API_CLASS_LIST = new ArrayList<>(); + public static final List> PROTOCOL_CLASS_LIST = new ArrayList<>(); + + public static void doLoad() { + { + IGNORED_FIELDS.put(ClientConfig.class, Sets.newHashSet("namesrvAddr", "clientIP", "clientCallbackExecutorThreads")); + IGNORED_FIELDS.put(DefaultLitePullConsumer.class, Sets.newHashSet("consumeTimestamp")); + IGNORED_FIELDS.put(DefaultMQPushConsumer.class, Sets.newHashSet("consumeTimestamp")); + FIELD_CLASS_NAMES.add(String.class.getName()); + FIELD_CLASS_NAMES.add(Long.class.getName()); + FIELD_CLASS_NAMES.add(Integer.class.getName()); + FIELD_CLASS_NAMES.add(Short.class.getName()); + FIELD_CLASS_NAMES.add(Byte.class.getName()); + FIELD_CLASS_NAMES.add(Double.class.getName()); + FIELD_CLASS_NAMES.add(Float.class.getName()); + FIELD_CLASS_NAMES.add(Boolean.class.getName()); + } + { + //basic + API_CLASS_LIST.add(DefaultMQPushConsumer.class); + API_CLASS_LIST.add(DefaultMQProducer.class); + API_CLASS_LIST.add(DefaultMQPullConsumer.class); + API_CLASS_LIST.add(DefaultLitePullConsumer.class); + API_CLASS_LIST.add(DefaultMQAdminExt.class); + + //argument + API_CLASS_LIST.add(Message.class); + API_CLASS_LIST.add(MessageQueue.class); + API_CLASS_LIST.add(SendCallback.class); + API_CLASS_LIST.add(PullCallback.class); + API_CLASS_LIST.add(MessageQueueSelector.class); + API_CLASS_LIST.add(AllocateMessageQueueStrategy.class); + //result + API_CLASS_LIST.add(MessageExt.class); + API_CLASS_LIST.add(SendResult.class); + API_CLASS_LIST.add(SendStatus.class); + API_CLASS_LIST.add(PullResult.class); + API_CLASS_LIST.add(PullStatus.class); + //listener and context + API_CLASS_LIST.add(MessageListener.class); + API_CLASS_LIST.add(MessageListenerConcurrently.class); + API_CLASS_LIST.add(ConsumeConcurrentlyContext.class); + API_CLASS_LIST.add(ConsumeConcurrentlyStatus.class); + API_CLASS_LIST.add(MessageListenerOrderly.class); + API_CLASS_LIST.add(ConsumeOrderlyContext.class); + API_CLASS_LIST.add(ConsumeOrderlyStatus.class); + //hook and context + API_CLASS_LIST.add(RPCHook.class); + API_CLASS_LIST.add(org.apache.rocketmq.client.hook.FilterMessageHook.class); + API_CLASS_LIST.add(org.apache.rocketmq.client.hook.SendMessageHook.class); + API_CLASS_LIST.add(org.apache.rocketmq.client.hook.CheckForbiddenHook.class); + API_CLASS_LIST.add(org.apache.rocketmq.client.hook.ConsumeMessageHook.class); + API_CLASS_LIST.add(org.apache.rocketmq.client.hook.EndTransactionHook.class); + API_CLASS_LIST.add(org.apache.rocketmq.client.hook.FilterMessageContext.class); + API_CLASS_LIST.add(org.apache.rocketmq.client.hook.SendMessageContext.class); + API_CLASS_LIST.add(org.apache.rocketmq.client.hook.ConsumeMessageContext.class); + API_CLASS_LIST.add(org.apache.rocketmq.client.hook.ConsumeMessageContext.class); + API_CLASS_LIST.add(org.apache.rocketmq.client.hook.EndTransactionContext.class); + + } + { + PROTOCOL_CLASS_LIST.add(RequestCode.class); + Reflections reflections = new Reflections("org.apache.rocketmq"); + for (Class protocolClass: reflections.getSubTypesOf(CommandCustomHeader.class)) { + PROTOCOL_CLASS_LIST.add(protocolClass); + } + } + + } + +} diff --git a/test/src/main/java/org/apache/rocketmq/test/schema/SchemaTools.java b/test/src/main/java/org/apache/rocketmq/test/schema/SchemaTools.java new file mode 100644 index 0000000..edd7de0 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/schema/SchemaTools.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.test.schema; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import static org.apache.rocketmq.test.schema.SchemaDefiner.FIELD_CLASS_NAMES; +import static org.apache.rocketmq.test.schema.SchemaDefiner.IGNORED_FIELDS; + +public class SchemaTools { + public static final String PATH_API = "api"; + public static final String PATH_PROTOCOL = "protocol"; + + public static String isPublicOrPrivate(int modifiers) { + if (Modifier.isPublic(modifiers)) { + return "public"; + } else if (Modifier.isProtected(modifiers)) { + return "protected"; + } else { + return "private"; + } + } + public static TreeMap buildSchemaOfFields(Class apiClass) throws Exception { + List fields = new ArrayList<>(); + Class current = apiClass; + do { + fields.addAll(Arrays.asList(current.getDeclaredFields())); + } while ((current = current.getSuperclass()) != null + && current != Object.class); + Object obj = null; + if (!apiClass.isInterface() + && !Modifier.isAbstract(apiClass.getModifiers()) + && !apiClass.isEnum()) { + Constructor constructor = null; + for (Constructor tmp : apiClass.getConstructors()) { + if (constructor == null) { + constructor = tmp; + } + if (tmp.getParameterCount() < constructor.getParameterCount()) { + constructor = tmp; + } + } + assert constructor != null; + constructor.setAccessible(true); + final String msg = constructor.getName(); + try { + obj = constructor.newInstance(Arrays.stream(constructor.getParameterTypes()).map(x -> { + try { + if (x.isEnum()) { + return x.getEnumConstants()[0]; + } if (x == boolean.class) { + return false; + } else if (x == char.class) { + return ""; + } else if (x.isPrimitive()) { + return 0; + } else { + return x.newInstance(); + } + } catch (InstantiationException instantiationException) { + return x.cast(null); + } catch (Exception e) { + throw new RuntimeException(msg + " " + x.getName(), e); + } + }).toArray()); + } catch (Exception e) { + throw new RuntimeException(msg, e); + } + } + TreeMap map = new TreeMap<>(); + if (apiClass.isEnum()) { + for (Object enumObject: apiClass.getEnumConstants()) { + String name = ((Enum)enumObject).name(); + int ordinal = ((Enum)enumObject).ordinal(); + String key = String.format("Field %s", name); + String value = String.format("%s %s %s", "public", "int", "" + ordinal); + map.put(key, value); + } + return map; + } + for (Field field: fields) { + if (field.getName().startsWith("$")) { + //inner fields + continue; + } + String key = String.format("Field %s", field.getName()); + boolean ignore = false; + for (Class tmpClass: IGNORED_FIELDS.keySet()) { + if (tmpClass.isAssignableFrom(apiClass) + && IGNORED_FIELDS.get(tmpClass).contains(field.getName())) { + ignore = true; + //System.out.printf("Ignore AAA:%s %s %s\n", apiClass.getName(), field.getName(), field.getType().getName()); + break; + } + } + if (!field.getType().isEnum() + && !field.getType().isPrimitive() + && !FIELD_CLASS_NAMES.contains(field.getType().getName())) { + //System.out.printf("Ignore BBB:%s %s %s\n", apiClass.getName(), field.getName(), field.getType().getName()); + ignore = true; + } + field.setAccessible(true); + Object fieldValue = "null"; + try { + fieldValue = field.get(obj); + } catch (Exception e) { + throw new RuntimeException(apiClass.getName() + " " + field.getName(), e); + } + if (ignore) { + //System.out.printf("Ignore:%s %s %s\n", apiClass.getName(), field.getName(), field.getType().getName()); + fieldValue = "null"; + } + String value = String.format("%s %s %s", isPublicOrPrivate(field.getModifiers()), field.getType().getName(), fieldValue); + map.put(key, value); + } + return map; + } + + public static TreeMap buildSchemaOfMethods(Class apiClass) throws Exception { + List methods = new ArrayList<>(); + Class current = apiClass; + do { + methods.addAll(Arrays.asList(current.getDeclaredMethods())); + } while ((current = current.getSuperclass()) != null + && current != Object.class); + TreeMap map = new TreeMap<>(); + if (apiClass.isEnum()) { + return map; + } + for (Method method: methods) { + if (!Modifier.isPublic(method.getModifiers())) { + //only care for the public methods + continue; + } + Class[] parameterTypes = method.getParameterTypes(); + Arrays.sort(parameterTypes, Comparator.comparing(Class::getName)); + Class[] exceptionTypes = method.getExceptionTypes(); + Arrays.sort(exceptionTypes, Comparator.comparing(Class::getName)); + String key = String.format("Method %s(%s)", method.getName(), Arrays.stream(parameterTypes).map(Class::getName).collect(Collectors.joining(","))); + String value = String.format("%s throws (%s): %s", + isPublicOrPrivate(method.getModifiers()), + method.getReturnType().getName(), + Arrays.stream(exceptionTypes).map(Class::getName).collect(Collectors.joining(","))); + map.put(key, value); + } + return map; + } + + + public static Map> generate(List> classList) throws Exception { + Map> schemaMap = new HashMap<>(); + for (Class apiClass : classList) { + TreeMap map = new TreeMap<>(); + map.putAll(buildSchemaOfFields(apiClass)); + map.putAll(buildSchemaOfMethods(apiClass)); + schemaMap.put(apiClass.getName().replace("org.apache.rocketmq.", ""), map); + } + return schemaMap; + } + + public static void write(Map> schemaMap, String base, String label) throws Exception { + for (Map.Entry> entry : schemaMap.entrySet()) { + TreeMap map = entry.getValue(); + final String fileName = String.format("%s/%s/%s.schema", base, label, entry.getKey()); + File file = new File(fileName); + FileOutputStream fileStream = new FileOutputStream(file); + Writer writer = new OutputStreamWriter(fileStream, StandardCharsets.UTF_8); + writer.write("/*\n" + + " * Licensed to the Apache Software Foundation (ASF) under one or more\n" + + " * contributor license agreements. See the NOTICE file distributed with\n" + + " * this work for additional information regarding copyright ownership.\n" + + " * The ASF licenses this file to You under the Apache License, Version 2.0\n" + + " * (the \"License\"); you may not use this file except in compliance with\n" + + " * the License. You may obtain a copy of the License at\n" + + " *\n" + + " * http://www.apache.org/licenses/LICENSE-2.0\n" + + " *\n" + + " * Unless required by applicable law or agreed to in writing, software\n" + + " * distributed under the License is distributed on an \"AS IS\" BASIS,\n" + + " * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" + + " * See the License for the specific language governing permissions and\n" + + " * limitations under the License.\n" + + " */\n\n\n"); + for (Map.Entry kv: map.entrySet()) { + writer.append(String.format("%s : %s\n", kv.getKey(), kv.getValue())); + } + writer.close(); + } + } + + public static Map> load(String base, String label) throws Exception { + File dir = new File(String.format("%s/%s", base, label)); + Map> schemaMap = new TreeMap<>(); + for (File file: dir.listFiles()) { + BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)); + String line = null; + TreeMap kvs = new TreeMap<>(); + while ((line = br.readLine()) != null) { + if (line.contains("*")) { + continue; + } + if (line.trim().isEmpty()) { + continue; + } + String[] items = line.split(":"); + kvs.put(items[0].trim(), items[1].trim()); + } + br.close(); + schemaMap.put(file.getName().replace(".schema", ""), kvs); + } + return schemaMap; + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/sendresult/ResultWrapper.java b/test/src/main/java/org/apache/rocketmq/test/sendresult/ResultWrapper.java new file mode 100644 index 0000000..9fe3146 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/sendresult/ResultWrapper.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.test.sendresult; + +public class ResultWrapper { + private boolean sendResult = false; + private String msgId = null; + private Exception sendException = null; + private String brokerIp = null; + + public String getBrokerIp() { + return brokerIp; + } + + public void setBrokerIp(String brokerIp) { + this.brokerIp = brokerIp; + } + + public boolean isSendResult() { + return sendResult; + } + + public void setSendResult(boolean sendResult) { + this.sendResult = sendResult; + } + + public String getMsgId() { + return msgId; + } + + public void setMsgId(String msgId) { + this.msgId = msgId; + } + + public Exception getSendException() { + return sendException; + } + + public void setSendException(Exception sendException) { + this.sendException = sendException; + } + + @Override + public String toString() { + return String.format("sendstatus:%s msgId:%s", sendResult, msgId); + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/util/Condition.java b/test/src/main/java/org/apache/rocketmq/test/util/Condition.java new file mode 100644 index 0000000..3c5f403 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/util/Condition.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.test.util; + +public interface Condition { + boolean meetCondition(); +} + diff --git a/test/src/main/java/org/apache/rocketmq/test/util/DuplicateMessageInfo.java b/test/src/main/java/org/apache/rocketmq/test/util/DuplicateMessageInfo.java new file mode 100644 index 0000000..8aa2840 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/util/DuplicateMessageInfo.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.test.util; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.text.DecimalFormat; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public class DuplicateMessageInfo { + + public void checkDuplicatedMessageInfo(boolean bPrintLog, + List> lQueueList) throws IOException { + int msgListSize = lQueueList.size(); + int maxmsgList = 0; + Map msgIdMap = new HashMap(); + Map dupMsgMap = new HashMap(); + + for (int i = 0; i < msgListSize; i++) { + if (maxmsgList < lQueueList.get(i).size()) + maxmsgList = lQueueList.get(i).size(); + } + + List strBQueue = new LinkedList(); + for (int i = 0; i < msgListSize; i++) + strBQueue.add(new StringBuilder()); + + for (int msgListIndex = 0; msgListIndex < maxmsgList; msgListIndex++) { + for (int msgQueueListIndex = 0; msgQueueListIndex < msgListSize; msgQueueListIndex++) { + if (msgListIndex < lQueueList.get(msgQueueListIndex).size()) { + if (msgIdMap.containsKey(lQueueList.get(msgQueueListIndex).get(msgListIndex))) { + if (dupMsgMap.containsKey(msgQueueListIndex)) { + int dupMsgCount = dupMsgMap.get(msgQueueListIndex); + dupMsgCount++; + dupMsgMap.remove(msgQueueListIndex); + dupMsgMap.put(msgQueueListIndex, dupMsgCount); + } else { + dupMsgMap.put(msgQueueListIndex, 1); + } + + strBQueue.get(msgQueueListIndex).append("" + msgQueueListIndex + "\t" + + msgIdMap.get(lQueueList.get(msgQueueListIndex).get(msgListIndex)) + "\t" + + lQueueList.get(msgQueueListIndex).get(msgListIndex) + "\r\n"); + } else { + msgIdMap.put(lQueueList.get(msgQueueListIndex).get(msgListIndex), msgQueueListIndex); + } + } + } + } + + int msgTotalNum = getMsgTotalNumber(lQueueList); + int msgTotalDupNum = getDuplicateMsgNum(dupMsgMap); + int msgNoDupNum = msgTotalNum - msgTotalDupNum; + float msgDupRate = ((float) msgTotalDupNum / (float) msgTotalNum) * 100.0f; + StringBuilder strBuilder = new StringBuilder(); + + strBuilder.append("msgTotalNum:" + msgTotalNum + "\r\n"); + strBuilder.append("msgTotalDupNum:" + msgTotalDupNum + "\r\n"); + strBuilder.append("msgNoDupNum:" + msgNoDupNum + "\r\n"); + strBuilder.append("msgDupRate" + getFloatNumString(msgDupRate) + "%\r\n"); + + strBuilder.append("queue\tmsg(dupNum/dupRate)\tdupRate\r\n"); + for (int i = 0; i < dupMsgMap.size(); i++) { + int msgDupNum = dupMsgMap.get(i); + int msgNum = lQueueList.get(i).size(); + float msgQueueDupRate = ((float) msgDupNum / (float) msgTotalDupNum) * 100.0f; + float msgQueueInnerDupRate = ((float) msgDupNum / (float) msgNum) * 100.0f; + + strBuilder.append(i + "\t" + msgDupNum + "/" + getFloatNumString(msgQueueDupRate) + "%" + "\t\t" + + getFloatNumString(msgQueueInnerDupRate) + "%\r\n"); + } + + System.out.print(strBuilder); + String titleString = "queue\tdupQueue\tdupMsg\r\n"; + System.out.print(titleString); + + for (int i = 0; i < msgListSize; i++) + System.out.print(strBQueue.get(i).toString()); + + if (bPrintLog) { + String logFileNameStr = "D:" + File.separator + "checkDuplicatedMessageInfo.txt"; + File logFileNameFile = new File(logFileNameStr); + OutputStream out = new FileOutputStream(logFileNameFile, true); + + String strToWrite; + byte[] byteToWrite; + strToWrite = strBuilder + titleString; + for (int i = 0; i < msgListSize; i++) + strToWrite += strBQueue.get(i).toString() + "\r\n"; + + byteToWrite = strToWrite.getBytes(StandardCharsets.UTF_8); + out.write(byteToWrite); + out.close(); + } + } + + private int getMsgTotalNumber(List> lQueueList) { + int msgTotalNum = 0; + for (int i = 0; i < lQueueList.size(); i++) { + msgTotalNum += lQueueList.get(i).size(); + } + return msgTotalNum; + } + + private int getDuplicateMsgNum(Map msgDupMap) { + int msgDupNum = 0; + for (int i = 0; i < msgDupMap.size(); i++) { + msgDupNum += msgDupMap.get(i); + } + return msgDupNum; + } + + private String getFloatNumString(float fNum) { + DecimalFormat dcmFmt = new DecimalFormat("0.00"); + return dcmFmt.format(fNum); + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/util/FileUtil.java b/test/src/main/java/org/apache/rocketmq/test/util/FileUtil.java new file mode 100644 index 0000000..0385728 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/util/FileUtil.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.test.util; + +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.Map.Entry; +import java.util.Properties; + +public class FileUtil { + private static String lineSeparator = System.getProperty("line.separator"); + + private String filePath = ""; + private String fileName = ""; + + public FileUtil(String filePath, String fileName) { + this.filePath = filePath; + this.fileName = fileName; + } + + public static void main(String[] args) { + String filePath = FileUtil.class.getResource("/").getPath(); + String fileName = "test.txt"; + FileUtil fileUtil = new FileUtil(filePath, fileName); + Properties properties = new Properties(); + properties.put("xx", "yy"); + properties.put("yy", "xx"); + fileUtil.writeProperties(properties); + } + + public void deleteFile() { + File file = new File(filePath + File.separator + fileName); + if (file.exists()) { + file.delete(); + } + } + + public void appendFile(String content) { + File file = openFile(); + String newContent = lineSeparator + content; + writeFile(file, newContent, true); + } + + public void coverFile(String content) { + File file = openFile(); + writeFile(file, content, false); + } + + public void writeProperties(Properties properties) { + String content = getPropertiesAsString(properties); + this.coverFile(content); + } + + private String getPropertiesAsString(Properties properties) { + StringBuilder sb = new StringBuilder(); + for (Entry keyEnty : properties.entrySet()) { + sb.append(keyEnty.getKey()).append("=").append((String) keyEnty.getValue()) + .append(lineSeparator); + } + return sb.toString(); + } + + private void writeFile(File file, String content, boolean append) { + Writer writer = null; + try { + FileOutputStream fileStream = new FileOutputStream(file, append); + writer = new OutputStreamWriter(fileStream, StandardCharsets.UTF_8); + writer.write(content); + writer.flush(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + private File openFile() { + File file = new File(filePath + File.separator + fileName); + if (!file.exists()) { + try { + file.createNewFile(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return file; + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/util/MQAdminTestUtils.java b/test/src/main/java/org/apache/rocketmq/test/util/MQAdminTestUtils.java new file mode 100644 index 0000000..276d08d --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/util/MQAdminTestUtils.java @@ -0,0 +1,358 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.test.util; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +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.common.MixAll; +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; +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.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicConfigAndQueueMapping; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingOne; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingUtils; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicRemappingDetailWrapper; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.remoting.rpc.ClientMetadata; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.admin.MQAdminUtils; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.topic.RemappingStaticTopicSubCommand; +import org.apache.rocketmq.tools.command.topic.UpdateStaticTopicSubCommand; + +import static org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingUtils.getMappingDetailFromConfig; +import static org.awaitility.Awaitility.await; + +public class MQAdminTestUtils { + private static Logger log = LoggerFactory.getLogger(MQAdminTestUtils.class); + + private static DefaultMQAdminExt mqAdminExt; + + public static void startAdmin(String nameSrvAddr) throws MQClientException { + mqAdminExt = new DefaultMQAdminExt(); + mqAdminExt.setNamesrvAddr(nameSrvAddr); + mqAdminExt.start(); + } + + public static void shutdownAdmin() { + mqAdminExt.shutdown(); + } + + public static boolean createTopic(String nameSrvAddr, String clusterName, String topic, + int queueNum, Map attributes) { + int defaultWaitTime = 30; + return createTopic(nameSrvAddr, clusterName, topic, queueNum, attributes, defaultWaitTime); + } + + public static boolean createTopic(String nameSrvAddr, String clusterName, String topic, + int queueNum, Map attributes, int waitTimeSec) { + DefaultMQAdminExt mqAdminExt = new DefaultMQAdminExt(); + mqAdminExt.setInstanceName(UUID.randomUUID().toString()); + mqAdminExt.setNamesrvAddr(nameSrvAddr); + try { + mqAdminExt.start(); + mqAdminExt.createTopic(clusterName, topic, queueNum, attributes); + } catch (Exception e) { + } + + await().atMost(waitTimeSec, TimeUnit.SECONDS).until(() -> checkTopicExist(mqAdminExt, topic)); + ForkJoinPool.commonPool().execute(mqAdminExt::shutdown); + return true; + } + + public static boolean checkTopicExist(DefaultMQAdminExt mqAdminExt, String topic) { + boolean createResult = false; + try { + TopicStatsTable topicInfo = mqAdminExt.examineTopicStats(topic); + createResult = !topicInfo.getOffsetTable().isEmpty(); + } catch (Exception e) { + } + + return createResult; + } + + public static boolean createSub(String nameSrvAddr, String clusterName, String consumerId) { + boolean createResult = true; + DefaultMQAdminExt mqAdminExt = new DefaultMQAdminExt(); + mqAdminExt.setNamesrvAddr(nameSrvAddr); + SubscriptionGroupConfig config = new SubscriptionGroupConfig(); + config.setGroupName(consumerId); + try { + mqAdminExt.start(); + Set masterSet = CommandUtil.fetchMasterAddrByClusterName(mqAdminExt, + clusterName); + for (String addr : masterSet) { + try { + mqAdminExt.createAndUpdateSubscriptionGroupConfig(addr, config); + log.info("create subscription group {} to {} success.", consumerId, addr); + } catch (Exception e) { + e.printStackTrace(); + Thread.sleep(1000 * 1); + } + } + } catch (Exception e) { + createResult = false; + e.printStackTrace(); + } + ForkJoinPool.commonPool().execute(mqAdminExt::shutdown); + return createResult; + } + + public static ClusterInfo getCluster(String nameSrvAddr) { + DefaultMQAdminExt mqAdminExt = new DefaultMQAdminExt(); + mqAdminExt.setNamesrvAddr(nameSrvAddr); + ClusterInfo clusterInfo = null; + try { + mqAdminExt.start(); + clusterInfo = mqAdminExt.examineBrokerClusterInfo(); + } catch (Exception e) { + e.printStackTrace(); + } + ForkJoinPool.commonPool().execute(mqAdminExt::shutdown); + return clusterInfo; + } + + public static boolean isBrokerExist(String ns, String ip) { + ClusterInfo clusterInfo = getCluster(ns); + if (clusterInfo == null) { + return false; + } else { + Map brokers = clusterInfo.getBrokerAddrTable(); + for (String brokerName : brokers.keySet()) { + HashMap brokerIps = brokers.get(brokerName).getBrokerAddrs(); + for (long brokerId : brokerIps.keySet()) { + if (brokerIps.get(brokerId).contains(ip)) + return true; + } + } + } + + return false; + } + + + public static boolean awaitStaticTopicMs(long timeMs, String topic, DefaultMQAdminExt defaultMQAdminExt, MQClientInstance clientInstance) throws Exception { + long start = System.currentTimeMillis(); + while (System.currentTimeMillis() - start <= timeMs) { + if (checkStaticTopic(topic, defaultMQAdminExt, clientInstance)) { + return true; + } + Thread.sleep(100); + } + return false; + } + + //Check if the client metadata is consistent with server metadata + public static boolean checkStaticTopic(String topic, DefaultMQAdminExt defaultMQAdminExt, MQClientInstance clientInstance) throws Exception { + Map brokerConfigMap = MQAdminUtils.examineTopicConfigAll(topic, defaultMQAdminExt); + assert !brokerConfigMap.isEmpty(); + TopicQueueMappingUtils.checkPhysicalQueueConsistence(brokerConfigMap); + TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, brokerConfigMap); + Map globalIdMap = TopicQueueMappingUtils.checkAndBuildMappingItems(getMappingDetailFromConfig(brokerConfigMap.values()), false, true); + for (int i = 0; i < globalIdMap.size(); i++) { + TopicQueueMappingOne mappingOne = globalIdMap.get(i); + String mockBrokerName = TopicQueueMappingUtils.getMockBrokerName(mappingOne.getMappingDetail().getScope()); + String bnameFromRoute = clientInstance.getBrokerNameFromMessageQueue(new MessageQueue(topic, mockBrokerName, mappingOne.getGlobalId())); + if (!mappingOne.getBname().equals(bnameFromRoute)) { + return false; + } + } + return true; + } + + //should only be test, if some middle operation failed, it does not backup the brokerConfigMap + public static Map createStaticTopic(String topic, int queueNum, Set targetBrokers, DefaultMQAdminExt defaultMQAdminExt) throws Exception { + Map brokerConfigMap = MQAdminUtils.examineTopicConfigAll(topic, defaultMQAdminExt); + assert brokerConfigMap.isEmpty(); + TopicQueueMappingUtils.createTopicConfigMapping(topic, queueNum, targetBrokers, brokerConfigMap); + MQAdminUtils.completeNoTargetBrokers(brokerConfigMap, defaultMQAdminExt); + MQAdminUtils.updateTopicConfigMappingAll(brokerConfigMap, defaultMQAdminExt, false); + return brokerConfigMap; + } + + //should only be test, if some middle operation failed, it does not backup the brokerConfigMap + public static void remappingStaticTopic(String topic, Set targetBrokers, DefaultMQAdminExt defaultMQAdminExt) throws Exception { + Map brokerConfigMap = MQAdminUtils.examineTopicConfigAll(topic, defaultMQAdminExt); + assert !brokerConfigMap.isEmpty(); + TopicRemappingDetailWrapper wrapper = TopicQueueMappingUtils.remappingStaticTopic(topic, brokerConfigMap, targetBrokers); + MQAdminUtils.completeNoTargetBrokers(brokerConfigMap, defaultMQAdminExt); + MQAdminUtils.remappingStaticTopic(topic, wrapper.getBrokerToMapIn(), wrapper.getBrokerToMapOut(), brokerConfigMap, TopicQueueMappingUtils.DEFAULT_BLOCK_SEQ_SIZE, false, defaultMQAdminExt); + } + + + //for test only + public static void remappingStaticTopicWithNegativeLogicOffset(String topic, Set targetBrokers, DefaultMQAdminExt defaultMQAdminExt) throws Exception { + Map brokerConfigMap = MQAdminUtils.examineTopicConfigAll(topic, defaultMQAdminExt); + assert !brokerConfigMap.isEmpty(); + TopicRemappingDetailWrapper wrapper = TopicQueueMappingUtils.remappingStaticTopic(topic, brokerConfigMap, targetBrokers); + MQAdminUtils.completeNoTargetBrokers(brokerConfigMap, defaultMQAdminExt); + remappingStaticTopicWithNegativeLogicOffset(topic, wrapper.getBrokerToMapIn(), wrapper.getBrokerToMapOut(), brokerConfigMap, TopicQueueMappingUtils.DEFAULT_BLOCK_SEQ_SIZE, false, defaultMQAdminExt); + } + + //for test only + public static void remappingStaticTopicWithNegativeLogicOffset(String topic, Set brokersToMapIn, Set brokersToMapOut, Map brokerConfigMap, int blockSeqSize, boolean force, DefaultMQAdminExt defaultMQAdminExt) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + + ClientMetadata clientMetadata = MQAdminUtils.getBrokerMetadata(defaultMQAdminExt); + MQAdminUtils.checkIfMasterAlive(brokerConfigMap.keySet(), defaultMQAdminExt, clientMetadata); + // now do the remapping + //Step1: let the new leader can be write without the logicOffset + for (String broker : brokersToMapIn) { + String addr = clientMetadata.findMasterBrokerAddr(broker); + TopicConfigAndQueueMapping configMapping = brokerConfigMap.get(broker); + defaultMQAdminExt.createStaticTopic(addr, defaultMQAdminExt.getCreateTopicKey(), configMapping, configMapping.getMappingDetail(), force); + } + //Step2: forbid the write of old leader + for (String broker : brokersToMapOut) { + String addr = clientMetadata.findMasterBrokerAddr(broker); + TopicConfigAndQueueMapping configMapping = brokerConfigMap.get(broker); + defaultMQAdminExt.createStaticTopic(addr, defaultMQAdminExt.getCreateTopicKey(), configMapping, configMapping.getMappingDetail(), force); + } + + //Step5: write the non-target brokers + for (String broker : brokerConfigMap.keySet()) { + if (brokersToMapIn.contains(broker) || brokersToMapOut.contains(broker)) { + continue; + } + String addr = clientMetadata.findMasterBrokerAddr(broker); + TopicConfigAndQueueMapping configMapping = brokerConfigMap.get(broker); + defaultMQAdminExt.createStaticTopic(addr, defaultMQAdminExt.getCreateTopicKey(), configMapping, configMapping.getMappingDetail(), force); + } + } + + public static void createStaticTopicWithCommand(String topic, int queueNum, Set brokers, String cluster, String nameservers) throws Exception { + UpdateStaticTopicSubCommand cmd = new UpdateStaticTopicSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] args; + if (cluster != null) { + args = new String[]{ + "-c", cluster, + "-t", topic, + "-qn", String.valueOf(queueNum), + "-n", nameservers + }; + } else { + String brokerStr = String.join(",", brokers); + args = new String[]{ + "-b", brokerStr, + "-t", topic, + "-qn", String.valueOf(queueNum), + "-n", nameservers + }; + } + final CommandLine commandLine = ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), args, cmd.buildCommandlineOptions(options), new DefaultParser()); + if (null == commandLine) { + return; + } + if (commandLine.hasOption('n')) { + String namesrvAddr = commandLine.getOptionValue('n'); + System.setProperty(MixAll.NAMESRV_ADDR_PROPERTY, namesrvAddr); + } + cmd.execute(commandLine, options, null); + } + + public static void remappingStaticTopicWithCommand(String topic, Set brokers, String cluster, String nameservers) throws Exception { + RemappingStaticTopicSubCommand cmd = new RemappingStaticTopicSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] args; + if (cluster != null) { + args = new String[]{ + "-c", cluster, + "-t", topic, + "-n", nameservers + }; + } else { + String brokerStr = String.join(",", brokers); + args = new String[]{ + "-b", brokerStr, + "-t", topic, + "-n", nameservers + }; + } + final CommandLine commandLine = ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), args, cmd.buildCommandlineOptions(options), new DefaultParser()); + if (null == commandLine) { + return; + } + if (commandLine.hasOption('n')) { + String namesrvAddr = commandLine.getOptionValue('n'); + System.setProperty(MixAll.NAMESRV_ADDR_PROPERTY, namesrvAddr); + } + cmd.execute(commandLine, options, null); + } + + public static ConsumeStats examineConsumeStats(String brokerAddr, String topic, String group) { + ConsumeStats consumeStats = null; + try { + consumeStats = mqAdminExt.examineConsumeStats(brokerAddr, group, topic, 3000); + } catch (Exception ignored) { + } + return consumeStats; + } + + /** + * Delete topic from broker only without cleaning route info from name server forwardly + * + * @param nameSrvAddr the namesrv addr to connect + * @param brokerName the specific broker + * @param topic the specific topic to delete + */ + public static void deleteTopicFromBrokerOnly(String nameSrvAddr, String brokerName, String topic) { + DefaultMQAdminExt mqAdminExt = new DefaultMQAdminExt(); + mqAdminExt.setNamesrvAddr(nameSrvAddr); + + try { + mqAdminExt.start(); + String brokerAddr = CommandUtil.fetchMasterAddrByBrokerName(mqAdminExt, brokerName); + mqAdminExt.deleteTopicInBroker(Collections.singleton(brokerAddr), topic); + } catch (Exception ignored) { + } finally { + mqAdminExt.shutdown(); + } + } + + public static TopicRouteData examineTopicRouteInfo(String nameSrvAddr, String topicName) { + DefaultMQAdminExt mqAdminExt = new DefaultMQAdminExt(); + mqAdminExt.setNamesrvAddr(nameSrvAddr); + TopicRouteData route = null; + try { + mqAdminExt.start(); + route = mqAdminExt.examineTopicRouteInfo(topicName); + } catch (Exception ignored) { + } finally { + mqAdminExt.shutdown(); + } + return route; + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/util/MQRandomUtils.java b/test/src/main/java/org/apache/rocketmq/test/util/MQRandomUtils.java new file mode 100644 index 0000000..1d82445 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/util/MQRandomUtils.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.test.util; + +public class MQRandomUtils { + public static String getRandomTopic() { + return RandomUtils.getStringByUUID(); + } + + public static String getRandomConsumerGroup() { + return RandomUtils.getStringByUUID(); + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/util/MQWait.java b/test/src/main/java/org/apache/rocketmq/test/util/MQWait.java new file mode 100644 index 0000000..1623136 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/util/MQWait.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.test.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.listener.AbstractListener; + +import static com.google.common.truth.Truth.assertThat; + +public class MQWait { + private static Logger logger = LoggerFactory.getLogger(MQWait.class); + + public static boolean waitConsumeAll(int timeoutMills, Collection allSendMsgs, + AbstractListener... listeners) { + boolean recvAll = false; + long startTime = System.currentTimeMillis(); + Collection noDupMsgs = new ArrayList(); + while (!recvAll) { + if ((System.currentTimeMillis() - startTime) < timeoutMills) { + noDupMsgs.clear(); + try { + for (AbstractListener listener : listeners) { + Collection recvMsgs = Collections + .synchronizedCollection(listener.getAllUndupMsgBody()); + noDupMsgs.addAll(VerifyUtils.getFilterdMessage(allSendMsgs, recvMsgs)); + } + } catch (Exception e) { + e.printStackTrace(); + } + + try { + assertThat(noDupMsgs).containsAllIn(allSendMsgs); + recvAll = true; + break; + } catch (Throwable e) { + } + TestUtil.waitForMonment(500); + } else { + logger.error(String.format( + "timeout but still not receive all messages,expectSize[%s],realSize[%s]", + allSendMsgs.size(), noDupMsgs.size())); + break; + } + } + + return recvAll; + } + + public static void setCondition(Condition condition, int waitTimeMills, int intervalMills) { + long startTime = System.currentTimeMillis(); + while (!condition.meetCondition()) { + if (System.currentTimeMillis() - startTime > waitTimeMills) { + logger.error("time out,but contidion still not meet!"); + break; + } else { + TestUtil.waitForMonment(intervalMills); + } + } + } + + public static void main(String[] args) { + + long start = System.currentTimeMillis(); + MQWait.setCondition(new Condition() { + int i = 0; + + public boolean meetCondition() { + i++; + return i == 100; + } + }, 10 * 1000, 200); + + } + +} diff --git a/test/src/main/java/org/apache/rocketmq/test/util/RandomUtil.java b/test/src/main/java/org/apache/rocketmq/test/util/RandomUtil.java new file mode 100644 index 0000000..5b62a63 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/util/RandomUtil.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.test.util; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Random; +import java.util.Set; +import java.util.UUID; + +public final class RandomUtil { + + private static final int UNICODE_START = '\u4E00'; + private static final int UNICODE_END = '\u9FA0'; + private static Random rd = new Random(); + + private RandomUtil() { + + } + + public static long getLong() { + return rd.nextLong(); + } + + public static long getLongMoreThanZero() { + long res = rd.nextLong(); + while (res <= 0) { + res = rd.nextLong(); + } + return res; + } + + public static long getLongLessThan(long n) { + long res = rd.nextLong(); + return res % n; + } + + public static long getLongMoreThanZeroLessThan(long n) { + long res = getLongLessThan(n); + while (res <= 0) { + res = getLongLessThan(n); + } + return res; + } + + public static long getLongBetween(long n, long m) { + if (m <= n) { + return n; + } + long res = getLongMoreThanZero(); + return n + res % (m - n); + } + + public static int getInteger() { + return rd.nextInt(); + } + + public static int getIntegerMoreThanZero() { + int res = rd.nextInt(); + while (res <= 0) { + res = rd.nextInt(); + } + return res; + } + + public static int getIntegerLessThan(int n) { + int res = rd.nextInt(); + return res % n; + } + + public static int getIntegerMoreThanZeroLessThan(int n) { + int res = rd.nextInt(n); + while (res == 0) { + res = rd.nextInt(n); + } + return res; + } + + public static int getIntegerBetween(int n, int m)// m��ֵ����Ϊ���أ� + { + if (m == n) { + return n; + } + int res = getIntegerMoreThanZero(); + return n + res % (m - n); + } + + private static char getChar(int[] arg) { + int size = arg.length; + int c = rd.nextInt(size / 2); + c = c * 2; + return (char) (getIntegerBetween(arg[c], arg[c + 1])); + } + + private static String getString(int n, int[] arg) { + StringBuilder res = new StringBuilder(); + for (int i = 0; i < n; i++) { + res.append(getChar(arg)); + } + return res.toString(); + } + + public static String getStringWithCharacter(int n) { + int[] arg = new int[] {'a', 'z' + 1, 'A', 'Z' + 1}; + return getString(n, arg); + } + + public static String getStringWithNumber(int n) { + int[] arg = new int[] {'0', '9' + 1}; + return getString(n, arg); + } + + public static String getStringWithNumAndCha(int n) { + int[] arg = new int[] {'a', 'z' + 1, 'A', 'Z' + 1, '0', '9' + 1}; + return getString(n, arg); + } + + public static String getStringShortenThan(int n) { + int len = getIntegerMoreThanZeroLessThan(n); + return getStringWithCharacter(len); + } + + public static String getStringWithNumAndChaShortenThan(int n) { + int len = getIntegerMoreThanZeroLessThan(n); + return getStringWithNumAndCha(len); + } + + public static String getStringBetween(int n, int m) { + int len = getIntegerBetween(n, m); + return getStringWithCharacter(len); + } + + public static String getStringWithNumAndChaBetween(int n, int m) { + int len = getIntegerBetween(n, m); + return getStringWithNumAndCha(len); + } + + public static String getStringWithPrefix(int n, String prefix) { + int len = prefix.length(); + if (n <= len) + return prefix; + else { + len = n - len; + StringBuilder res = new StringBuilder(prefix); + res.append(getStringWithCharacter(len)); + return res.toString(); + } + } + + public static String getStringWithSuffix(int n, String suffix) { + + int len = suffix.length(); + if (n <= len) + return suffix; + else { + len = n - len; + StringBuilder res = new StringBuilder(); + res.append(getStringWithCharacter(len)); + res.append(suffix); + return res.toString(); + } + } + + public static String getStringWithBoth(int n, String prefix, String suffix) { + int len = prefix.length() + suffix.length(); + StringBuilder res = new StringBuilder(prefix); + if (n <= len) + return res.append(suffix).toString(); + else { + len = n - len; + res.append(getStringWithCharacter(len)); + res.append(suffix); + return res.toString(); + } + } + + public static String getCheseWordWithPrifix(int n, String prefix) { + int len = prefix.length(); + if (n <= len) + return prefix; + else { + len = n - len; + StringBuilder res = new StringBuilder(prefix); + res.append(getCheseWord(len)); + return res.toString(); + } + } + + public static String getCheseWordWithSuffix(int n, String suffix) { + + int len = suffix.length(); + if (n <= len) + return suffix; + else { + len = n - len; + StringBuilder res = new StringBuilder(); + res.append(getCheseWord(len)); + res.append(suffix); + return res.toString(); + } + } + + public static String getCheseWordWithBoth(int n, String prefix, String suffix) { + int len = prefix.length() + suffix.length(); + StringBuilder res = new StringBuilder(prefix); + if (n <= len) + return res.append(suffix).toString(); + else { + len = n - len; + res.append(getCheseWord(len)); + res.append(suffix); + return res.toString(); + } + } + + public static String getCheseWord(int len) { + StringBuilder res = new StringBuilder(); + for (int i = 0; i < len; i++) { + char str = getCheseChar(); + res.append(str); + } + return res.toString(); + } + + private static char getCheseChar() { + return (char) (UNICODE_START + rd.nextInt(UNICODE_END - UNICODE_START)); + } + + public static boolean getBoolean() { + return getIntegerMoreThanZeroLessThan(3) == 1; + } + + public static String getStringByUUID() { + return UUID.randomUUID().toString(); + } + + public static int[] getRandomArray(int min, int max, int n) { + int len = max - min + 1; + + if (max < min || n > len) { + return null; + } + + int[] source = new int[len]; + for (int i = min; i < min + len; i++) { + source[i - min] = i; + } + + int[] result = new int[n]; + Random rd = new Random(); + int index = 0; + for (int i = 0; i < result.length; i++) { + index = rd.nextInt(len--); + result[i] = source[index]; + source[index] = source[len]; + } + return result; + } + + public static Collection getRandomCollection(int min, int max, int n) { + Set res = new HashSet(); + int mx = max; + int mn = min; + if (n == (max + 1 - min)) { + for (int i = 1; i <= n; i++) { + res.add(i); + } + return res; + } + for (int i = 0; i < n; i++) { + int v = getIntegerBetween(mn, mx); + if (v == mx) { + mx--; + } + if (v == mn) { + mn++; + } + while (res.contains(v)) { + v = getIntegerBetween(mn, mx); + if (v == mx) { + mx = v; + } + if (v == mn) { + mn = v; + } + } + res.add(v); + } + return res; + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/util/RandomUtils.java b/test/src/main/java/org/apache/rocketmq/test/util/RandomUtils.java new file mode 100644 index 0000000..3f71176 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/util/RandomUtils.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.test.util; + +import java.util.Random; +import java.util.UUID; + +public class RandomUtils { + private static final int UNICODE_START = '\u4E00'; + private static final int UNICODE_END = '\u9FA0'; + private static Random rd = new Random(); + + private RandomUtils() { + + } + + public static String getStringByUUID() { + return UUID.randomUUID().toString(); + } + + public static String getCheseWord(int len) { + StringBuilder res = new StringBuilder(); + + for (int i = 0; i < len; ++i) { + char str = getCheseChar(); + res.append(str); + } + + return res.toString(); + } + + public static String getStringWithNumber(int n) { + int[] arg = new int[] {'0', '9' + 1}; + return getString(n, arg); + } + + public static String getStringWithCharacter(int n) { + int[] arg = new int[] {'a', 'z' + 1, 'A', 'Z' + 1}; + return getString(n, arg); + } + + private static String getString(int n, int[] arg) { + StringBuilder res = new StringBuilder(); + for (int i = 0; i < n; i++) { + res.append(getChar(arg)); + } + return res.toString(); + } + + private static char getChar(int[] arg) { + int size = arg.length; + int c = rd.nextInt(size / 2); + c = c * 2; + return (char) (getIntegerBetween(arg[c], arg[c + 1])); + } + + public static int getIntegerBetween(int n, int m) { + if (m == n) { + return n; + } + int res = getIntegerMoreThanZero(); + return n + res % (m - n); + } + + public static int getIntegerMoreThanZero() { + int res = rd.nextInt(); + while (res <= 0) { + res = rd.nextInt(); + } + return res; + } + + private static char getCheseChar() { + return (char) (UNICODE_START + rd.nextInt(UNICODE_END - UNICODE_START)); + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/util/StatUtil.java b/test/src/main/java/org/apache/rocketmq/test/util/StatUtil.java new file mode 100644 index 0000000..080b7e3 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/util/StatUtil.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.test.util; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +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 java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import javax.annotation.Generated; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +import static java.math.BigDecimal.ROUND_HALF_UP; + +@Generated("StatUtil") +public class StatUtil { + private static Logger sysLogger = LoggerFactory.getLogger(StatUtil.class); + private static Logger logger = LoggerFactory.getLogger("StatLogger"); + private static final int MAX_KEY_NUM = Integer.parseInt(System.getProperty("stat.util.key.max.num", "10000")); + private static volatile ConcurrentMap invokeCache = new ConcurrentHashMap<>(64); + private static volatile ConcurrentMap> secondInvokeCache = new ConcurrentHashMap<>( + 64); + + private static final int STAT_WINDOW_SECONDS = Integer.parseInt(System.getProperty("stat.win.seconds", "60")); + private static final String SPLITTER = "|"; + private static ScheduledExecutorService daemon = Executors.newSingleThreadScheduledExecutor(); + + static class Invoke { + AtomicLong totalPv = new AtomicLong(); + AtomicLong failPv = new AtomicLong(); + AtomicLong sumRt = new AtomicLong(); + AtomicLong maxRt = new AtomicLong(); + AtomicLong minRt = new AtomicLong(); + AtomicInteger topSecondPv = new AtomicInteger(); + AtomicInteger secondPv = new AtomicInteger(); + AtomicLong second = new AtomicLong(System.currentTimeMillis() / 1000L); + } + + static class SecondInvoke implements Comparable { + AtomicLong total = new AtomicLong(); + AtomicLong fail = new AtomicLong(); + AtomicLong sumRt = new AtomicLong(); + AtomicLong maxRt = new AtomicLong(); + AtomicLong minRt = new AtomicLong(); + Long second = nowSecond(); + + @Override + public int compareTo(SecondInvoke o) { + return o.second.compareTo(second); + } + } + + static { + daemon.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + printInvokeStat(); + printSecondInvokeStat(); + } catch (Exception e) { + logger.error("", e); + } + } + }, STAT_WINDOW_SECONDS, STAT_WINDOW_SECONDS, TimeUnit.SECONDS); + } + + private static void printInvokeStat() { + Map tmp = invokeCache; + invokeCache = new ConcurrentHashMap<>(64); + + sysLogger.warn("printInvokeStat key count:{}", tmp.size()); + for (Map.Entry entry : tmp.entrySet()) { + String key = entry.getKey(); + Invoke invoke = entry.getValue(); + logger.warn("{}", + buildLog(key, invoke.topSecondPv.get(), invoke.totalPv.get(), invoke.failPv.get(), invoke.minRt.get(), + invoke.maxRt.get(), invoke.sumRt.get())); + } + } + + private static void printSecondInvokeStat() { + sysLogger.warn("printSecondInvokeStat key count:{}", secondInvokeCache.size()); + for (Map.Entry> entry : secondInvokeCache.entrySet()) { + String key = entry.getKey(); + Map secondInvokeMap = entry.getValue(); + long totalPv = 0L; + long failPv = 0L; + long topSecondPv = 0L; + long sumRt = 0L; + long maxRt = 0L; + long minRt = 0L; + + for (Map.Entry invokeEntry : secondInvokeMap.entrySet()) { + long second = invokeEntry.getKey(); + SecondInvoke secondInvoke = invokeEntry.getValue(); + if (nowSecond() - second >= STAT_WINDOW_SECONDS) { + secondInvokeMap.remove(second); + continue; + } + long secondPv = secondInvoke.total.get(); + totalPv += secondPv; + failPv += secondInvoke.fail.get(); + sumRt += secondInvoke.sumRt.get(); + if (maxRt < secondInvoke.maxRt.get()) { + maxRt = secondInvoke.maxRt.get(); + } + if (minRt > secondInvoke.minRt.get()) { + minRt = secondInvoke.minRt.get(); + } + if (topSecondPv < secondPv) { + topSecondPv = secondPv; + } + } + if (secondInvokeMap.isEmpty()) { + secondInvokeCache.remove(key); + continue; + } + logger.warn("{}", buildLog(key, topSecondPv, totalPv, failPv, minRt, maxRt, sumRt)); + } + } + + private static String buildLog(String key, long topSecondPv, long totalPv, long failPv, long minRt, long maxRt, + long sumRt) { + StringBuilder sb = new StringBuilder(); + sb.append(SPLITTER); + sb.append(key); + sb.append(SPLITTER); + sb.append(topSecondPv); + sb.append(SPLITTER); + int tps = new BigDecimal(totalPv).divide(new BigDecimal(STAT_WINDOW_SECONDS), + ROUND_HALF_UP).intValue(); + sb.append(tps); + sb.append(SPLITTER); + sb.append(totalPv); + sb.append(SPLITTER); + sb.append(failPv); + sb.append(SPLITTER); + sb.append(minRt); + sb.append(SPLITTER); + long avg = new BigDecimal(sumRt).divide(new BigDecimal(totalPv), + ROUND_HALF_UP).longValue(); + sb.append(avg); + sb.append(SPLITTER); + sb.append(maxRt); + return sb.toString(); + } + + public static String buildKey(String... keys) { + if (keys == null || keys.length <= 0) { + return null; + } + StringBuilder sb = new StringBuilder(); + for (String key : keys) { + sb.append(key); + sb.append(","); + } + sb.deleteCharAt(sb.length() - 1); + return sb.toString(); + } + + public static void addInvoke(String key, long rt) { + addInvoke(key, rt, true); + } + + private static Invoke getAndSetInvoke(String key) { + Invoke invoke = invokeCache.get(key); + if (invoke == null) { + invokeCache.putIfAbsent(key, new Invoke()); + } + return invokeCache.get(key); + } + + public static void addInvoke(String key, int num, long rt, boolean success) { + if (invokeCache.size() > MAX_KEY_NUM || secondInvokeCache.size() > MAX_KEY_NUM) { + return; + } + Invoke invoke = getAndSetInvoke(key); + if (invoke == null) { + return; + } + + invoke.totalPv.getAndAdd(num); + if (!success) { + invoke.failPv.getAndAdd(num); + } + long now = nowSecond(); + AtomicLong oldSecond = invoke.second; + if (oldSecond.get() == now) { + invoke.secondPv.getAndAdd(num); + } else { + if (oldSecond.compareAndSet(oldSecond.get(), now)) { + if (invoke.secondPv.get() > invoke.topSecondPv.get()) { + invoke.topSecondPv.set(invoke.secondPv.get()); + } + invoke.secondPv.set(num); + } else { + invoke.secondPv.getAndAdd(num); + } + } + + invoke.sumRt.addAndGet(rt); + if (invoke.maxRt.get() < rt) { + invoke.maxRt.set(rt); + } + if (invoke.minRt.get() > rt) { + invoke.minRt.set(rt); + } + } + + public static void addInvoke(String key, long rt, boolean success) { + if (invokeCache.size() > MAX_KEY_NUM || secondInvokeCache.size() > MAX_KEY_NUM) { + return; + } + Invoke invoke = getAndSetInvoke(key); + if (invoke == null) { + return; + } + + invoke.totalPv.getAndIncrement(); + if (!success) { + invoke.failPv.getAndIncrement(); + } + long now = nowSecond(); + AtomicLong oldSecond = invoke.second; + if (oldSecond.get() == now) { + invoke.secondPv.getAndIncrement(); + } else { + if (oldSecond.compareAndSet(oldSecond.get(), now)) { + if (invoke.secondPv.get() > invoke.topSecondPv.get()) { + invoke.topSecondPv.set(invoke.secondPv.get()); + } + invoke.secondPv.set(1); + } else { + invoke.secondPv.getAndIncrement(); + } + } + + invoke.sumRt.addAndGet(rt); + if (invoke.maxRt.get() < rt) { + invoke.maxRt.set(rt); + } + if (invoke.minRt.get() > rt) { + invoke.minRt.set(rt); + } + } + + public static SecondInvoke getAndSetSecondInvoke(String key) { + if (!secondInvokeCache.containsKey(key)) { + secondInvokeCache.putIfAbsent(key, new ConcurrentHashMap<>(STAT_WINDOW_SECONDS)); + } + Map secondInvokeMap = secondInvokeCache.get(key); + if (secondInvokeMap == null) { + return null; + } + long second = nowSecond(); + if (!secondInvokeMap.containsKey(second)) { + secondInvokeMap.putIfAbsent(second, new SecondInvoke()); + } + return secondInvokeMap.get(second); + } + + public static void addSecondInvoke(String key, long rt) { + addSecondInvoke(key, rt, true); + } + + public static void addSecondInvoke(String key, long rt, boolean success) { + if (invokeCache.size() > MAX_KEY_NUM || secondInvokeCache.size() > MAX_KEY_NUM) { + return; + } + SecondInvoke secondInvoke = getAndSetSecondInvoke(key); + if (secondInvoke == null) { + return; + } + secondInvoke.total.addAndGet(1); + if (!success) { + secondInvoke.fail.addAndGet(1); + } + secondInvoke.sumRt.addAndGet(rt); + if (secondInvoke.maxRt.get() < rt) { + secondInvoke.maxRt.set(rt); + } + if (secondInvoke.minRt.get() > rt) { + secondInvoke.minRt.set(rt); + } + } + + public static void addPv(String key, long totalPv) { + addPv(key, totalPv, true); + } + + public static void addPv(String key, long totalPv, boolean success) { + if (invokeCache.size() > MAX_KEY_NUM || secondInvokeCache.size() > MAX_KEY_NUM) { + return; + } + if (totalPv <= 0) { + return; + } + Invoke invoke = getAndSetInvoke(key); + if (invoke == null) { + return; + } + invoke.totalPv.addAndGet(totalPv); + if (!success) { + invoke.failPv.addAndGet(totalPv); + } + long now = nowSecond(); + AtomicLong oldSecond = invoke.second; + if (oldSecond.get() == now) { + invoke.secondPv.addAndGet((int)totalPv); + } else { + if (oldSecond.compareAndSet(oldSecond.get(), now)) { + if (invoke.secondPv.get() > invoke.topSecondPv.get()) { + invoke.topSecondPv.set(invoke.secondPv.get()); + } + invoke.secondPv.set((int)totalPv); + } else { + invoke.secondPv.addAndGet((int)totalPv); + } + } + } + + public static void addSecondPv(String key, long totalPv) { + addSecondPv(key, totalPv, true); + } + + public static void addSecondPv(String key, long totalPv, boolean success) { + if (invokeCache.size() > MAX_KEY_NUM || secondInvokeCache.size() > MAX_KEY_NUM) { + return; + } + if (totalPv <= 0) { + return; + } + SecondInvoke secondInvoke = getAndSetSecondInvoke(key); + if (secondInvoke == null) { + return; + } + secondInvoke.total.addAndGet(totalPv); + if (!success) { + secondInvoke.fail.addAndGet(totalPv); + } + } + + public static boolean isOverFlow(String key, int tps) { + return nowTps(key) >= tps; + } + + public static int nowTps(String key) { + Map secondInvokeMap = secondInvokeCache.get(key); + if (secondInvokeMap != null) { + SecondInvoke secondInvoke = secondInvokeMap.get(nowSecond()); + if (secondInvoke != null) { + return (int)secondInvoke.total.get(); + } + } + Invoke invoke = invokeCache.get(key); + if (invoke == null) { + return 0; + } + AtomicLong oldSecond = invoke.second; + if (oldSecond.get() == nowSecond()) { + return invoke.secondPv.get(); + } + return 0; + } + + public static int totalPvInWindow(String key, int windowSeconds) { + List list = secondInvokeList(key, windowSeconds); + long totalPv = 0; + for (int i = 0; i < windowSeconds && i < list.size(); i++) { + totalPv += list.get(i).total.get(); + } + return (int)totalPv; + } + + public static int failPvInWindow(String key, int windowSeconds) { + List list = secondInvokeList(key, windowSeconds); + long failPv = 0; + for (int i = 0; i < windowSeconds && i < list.size(); i++) { + failPv += list.get(i).fail.get(); + } + return (int)failPv; + } + + public static int topTpsInWindow(String key, int windowSeconds) { + List list = secondInvokeList(key, windowSeconds); + long topTps = 0; + for (int i = 0; i < windowSeconds && i < list.size(); i++) { + long secondPv = list.get(i).total.get(); + if (topTps < secondPv) { + topTps = secondPv; + } + } + return (int)topTps; + } + + public static int avgRtInWindow(String key, int windowSeconds) { + List list = secondInvokeList(key, windowSeconds); + long sumRt = 0; + long totalPv = 0; + for (int i = 0; i < windowSeconds && i < list.size(); i++) { + sumRt += list.get(i).sumRt.get(); + totalPv += list.get(i).total.get(); + } + if (totalPv <= 0) { + return 0; + } + long avg = new BigDecimal(sumRt).divide(new BigDecimal(totalPv), + ROUND_HALF_UP).longValue(); + return (int)avg; + } + + public static int maxRtInWindow(String key, int windowSeconds) { + List list = secondInvokeList(key, windowSeconds); + long maxRt = 0; + long totalPv = 0; + for (int i = 0; i < windowSeconds && i < list.size(); i++) { + if (maxRt < list.get(i).maxRt.get()) { + maxRt = list.get(i).maxRt.get(); + } + } + return (int)maxRt; + } + + public static int minRtInWindow(String key, int windowSeconds) { + List list = secondInvokeList(key, windowSeconds); + long minRt = 0; + long totalPv = 0; + for (int i = 0; i < windowSeconds && i < list.size(); i++) { + if (minRt < list.get(i).minRt.get()) { + minRt = list.get(i).minRt.get(); + } + } + return (int)minRt; + } + + private static List secondInvokeList(String key, int windowSeconds) { + if (windowSeconds > STAT_WINDOW_SECONDS || windowSeconds <= 0) { + throw new IllegalArgumentException("windowSeconds Must Not be great than " + STAT_WINDOW_SECONDS); + } + Map secondInvokeMap = secondInvokeCache.get(key); + if (secondInvokeMap == null || secondInvokeMap.isEmpty()) { + return new ArrayList<>(); + } + List list = new ArrayList<>(); + list.addAll(secondInvokeMap.values()); + Collections.sort(list); + return list; + } + + private static long nowSecond() { + return System.currentTimeMillis() / 1000L; + } + +} diff --git a/test/src/main/java/org/apache/rocketmq/test/util/TestUtil.java b/test/src/main/java/org/apache/rocketmq/test/util/TestUtil.java new file mode 100644 index 0000000..1013759 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/util/TestUtil.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.test.util; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public final class TestUtil { + + private TestUtil() { + } + + public static Long parseStringToLong(String s, Long defval) { + Long val = defval; + try { + val = Long.parseLong(s); + } catch (NumberFormatException e) { + val = defval; + } + return val; + } + + public static Integer parseStringToInteger(String s, Integer defval) { + Integer val = defval; + try { + val = Integer.parseInt(s); + } catch (NumberFormatException e) { + val = defval; + } + return val; + } + + public static String addQuoteToParamater(String param) { + StringBuilder sb = new StringBuilder("'"); + sb.append(param).append("'"); + return sb.toString(); + } + + public static void waitForMonment(long time) { + try { + Thread.sleep(time); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public static void waitForSeconds(long time) { + try { + TimeUnit.SECONDS.sleep(time); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public static void waitForMinutes(long time) { + try { + TimeUnit.MINUTES.sleep(time); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public static void waitForInputQuit() { + waitForInput("quit"); + } + + public static void waitForInput(String keyWord) { + waitForInput(keyWord, + String.format("The thread will wait until you input stop command[%s]:", keyWord)); + } + + public static void waitForInput(String keyWord, String info) { + try { + byte[] b = new byte[1024]; + int n = System.in.read(b); + String s = new String(b, 0, n - 1, StandardCharsets.UTF_8).replace("\r", "").replace("\n", ""); + while (!s.equals(keyWord)) { + n = System.in.read(b); + s = new String(b, 0, n - 1, StandardCharsets.UTF_8); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static > Map sortByValue(Map map) { + List> list = new LinkedList>(map.entrySet()); + list.sort(Map.Entry.comparingByValue()); + + Map result = new LinkedHashMap(); + for (Map.Entry entry : list) { + result.put(entry.getKey(), entry.getValue()); + } + return result; + } + +} diff --git a/test/src/main/java/org/apache/rocketmq/test/util/TestUtils.java b/test/src/main/java/org/apache/rocketmq/test/util/TestUtils.java new file mode 100644 index 0000000..3eb1f7d --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/util/TestUtils.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.test.util; + +import java.util.concurrent.TimeUnit; + +public class TestUtils { + public static void waitForMoment(long time) { + try { + Thread.sleep(time); + } catch (InterruptedException var3) { + var3.printStackTrace(); + } + + } + + public static void waitForSeconds(long time) { + try { + TimeUnit.SECONDS.sleep(time); + } catch (InterruptedException var3) { + var3.printStackTrace(); + } + + } + + public static void waitForMinutes(long time) { + try { + TimeUnit.MINUTES.sleep(time); + } catch (InterruptedException var3) { + var3.printStackTrace(); + } + + } + +} diff --git a/test/src/main/java/org/apache/rocketmq/test/util/VerifyUtils.java b/test/src/main/java/org/apache/rocketmq/test/util/VerifyUtils.java new file mode 100644 index 0000000..aa842c5 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/util/VerifyUtils.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.test.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +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 VerifyUtils { + private static Logger logger = LoggerFactory.getLogger(VerifyUtils.class); + + public static int verify(Collection sendMsgs, Collection recvMsgs) { + int miss = 0; + for (Object msg : sendMsgs) { + if (!recvMsgs.contains(msg)) { + miss++; + } + } + + return miss; + } + + public static Collection getFilterdMessage(Collection sendMsgs, + Collection recvMsgs) { + Collection recvMsgsSync = Collections.synchronizedCollection(recvMsgs); + Collection filteredMsgs = new ArrayList(); + int filterNum = 0; + for (Object msg : recvMsgsSync) { + if (sendMsgs.contains(msg)) { + filteredMsgs.add(msg); + } else { + filterNum++; + } + } + + logger.info(String.format("[%s] messages is filtered!", filterNum)); + return filteredMsgs; + } + + public static int verifyUserProperty(Collection sendMsgs, Collection recvMsgs) { + return 0; + } + + public static void verifyMessageQueueId(int expectId, Collection msgs) { + for (Object msg : msgs) { + MessageExt msgEx = (MessageExt) msg; + assert expectId == msgEx.getQueueId(); + } + } + + public static boolean verifyBalance(int msgSize, float error, int... recvSize) { + boolean balance = true; + int evenSize = msgSize / recvSize.length; + for (int size : recvSize) { + if (Math.abs(size - evenSize) > error * evenSize) { + balance = false; + break; + } + } + return balance; + } + + public static boolean verifyBalance(int msgSize, int... recvSize) { + return verifyBalance(msgSize, 0.1f, recvSize); + } + + public static boolean verifyDelay(long delayTimeMills, long nextLevelDelayTimeMills, + Collection recvMsgTimes) { + boolean delay = true; + for (Object timeObj : recvMsgTimes) { + long time = (Long) timeObj; + if (time < delayTimeMills || time > nextLevelDelayTimeMills) { + delay = false; + logger.info(String.format("delay error:%s", Math.abs(time - delayTimeMills))); + break; + } + } + return delay; + } + + public static boolean verifyOrder(Collection> queueMsgs) { + for (Collection msgs : queueMsgs) { + if (!verifyOrderMsg(msgs)) { + return false; + } + } + return true; + + } + + public static boolean verifyOrderMsg(Collection msgs) { + int min = Integer.MIN_VALUE; + int curr; + if (msgs.size() == 0 || msgs.size() == 1) { + return true; + } else { + for (Object msg : msgs) { + curr = Integer.parseInt((String) msg); + if (curr < min) { + return false; + } else { + min = curr; + } + } + } + return true; + } + + public static boolean verifyRT(Collection rts, long maxRTMills) { + boolean rtExpect = true; + for (Object obj : rts) { + long rt = (Long) obj; + if (rt > maxRTMills) { + rtExpect = false; + logger.info(String.format("%s greater thran maxtRT:%s!", rt, maxRTMills)); + + } + } + return rtExpect; + } + + public static void main(String[] args) { + verifyBalance(400, 0.1f, 230, 190); + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/util/data/collect/DataCollector.java b/test/src/main/java/org/apache/rocketmq/test/util/data/collect/DataCollector.java new file mode 100644 index 0000000..cbbc8a5 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/util/data/collect/DataCollector.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.test.util.data.collect; + +import java.util.Collection; + +public interface DataCollector { + + void resetData(); + + Collection getAllData(); + + Collection getAllDataWithoutDuplicate(); + + void addData(Object data); + + long getDataSizeWithoutDuplicate(); + + long getDataSize(); + + boolean isRepeatedData(Object data); + + int getRepeatedTimeForData(Object data); + + void removeData(Object data); + + void lockIncrement(); + + void unlockIncrement(); +} diff --git a/test/src/main/java/org/apache/rocketmq/test/util/data/collect/DataCollectorManager.java b/test/src/main/java/org/apache/rocketmq/test/util/data/collect/DataCollectorManager.java new file mode 100644 index 0000000..47f4d81 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/util/data/collect/DataCollectorManager.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.test.util.data.collect; + +import java.util.HashMap; +import java.util.Map; +import org.apache.rocketmq.test.util.data.collect.impl.ListDataCollectorImpl; +import org.apache.rocketmq.test.util.data.collect.impl.MapDataCollectorImpl; + +public final class DataCollectorManager { + private static DataCollectorManager instance = new DataCollectorManager(); + private Map collectMap = new HashMap(); + private Object lock = new Object(); + + private DataCollectorManager() { + } + + public static DataCollectorManager getInstance() { + return instance; + } + + public DataCollector fetchDataCollector(String key) { + String realKey = key; + if (!collectMap.containsKey(realKey)) { + synchronized (lock) { + if (!collectMap.containsKey(realKey)) { + DataCollector collect = (DataCollector) new MapDataCollectorImpl(); + collectMap.put(realKey, collect); + } + } + } + return collectMap.get(realKey); + } + + public DataCollector fetchMapDataCollector(String key) { + String realKey = key; + if (!collectMap.containsKey(realKey) + || collectMap.get(realKey) instanceof ListDataCollectorImpl) { + synchronized (lock) { + if (!collectMap.containsKey(realKey) + || collectMap.get(realKey) instanceof ListDataCollectorImpl) { + DataCollector collect = null; + if (collectMap.containsKey(realKey)) { + DataCollector src = collectMap.get(realKey); + collect = new MapDataCollectorImpl(src.getAllData()); + } else { + collect = new MapDataCollectorImpl(); + } + collectMap.put(realKey, collect); + + } + } + } + return collectMap.get(realKey); + } + + public DataCollector fetchListDataCollector(String key) { + String realKey = key; + if (!collectMap.containsKey(realKey) + || collectMap.get(realKey) instanceof MapDataCollectorImpl) { + synchronized (lock) { + if (!collectMap.containsKey(realKey) + || collectMap.get(realKey) instanceof MapDataCollectorImpl) { + DataCollector collect = null; + if (collectMap.containsKey(realKey)) { + DataCollector src = collectMap.get(realKey); + collect = new ListDataCollectorImpl(src.getAllData()); + } else { + collect = new ListDataCollectorImpl(); + } + collectMap.put(realKey, collect); + } + } + } + return collectMap.get(realKey); + } + + public void resetDataCollect(String key) { + if (collectMap.containsKey(key)) { + collectMap.get(key).resetData(); + } + } + + public void resetAll() { + for (Map.Entry entry : collectMap.entrySet()) { + entry.getValue().resetData(); + } + } + + public void removeDataCollect(String key) { + collectMap.remove(key); + } + + public void removeAll() { + collectMap.clear(); + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/util/data/collect/DataFilter.java b/test/src/main/java/org/apache/rocketmq/test/util/data/collect/DataFilter.java new file mode 100644 index 0000000..b01adc5 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/util/data/collect/DataFilter.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.test.util.data.collect; + +public interface DataFilter { + +} diff --git a/test/src/main/java/org/apache/rocketmq/test/util/data/collect/impl/ListDataCollectorImpl.java b/test/src/main/java/org/apache/rocketmq/test/util/data/collect/impl/ListDataCollectorImpl.java new file mode 100644 index 0000000..b0a1ee3 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/util/data/collect/impl/ListDataCollectorImpl.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.test.util.data.collect.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import org.apache.rocketmq.test.util.data.collect.DataCollector; + +public class ListDataCollectorImpl implements DataCollector { + + private List datas = new ArrayList(); + private boolean lock = false; + + public ListDataCollectorImpl() { + + } + + public ListDataCollectorImpl(Collection datas) { + for (Object data : datas) { + addData(data); + } + } + + @Override + public Collection getAllData() { + return datas; + } + + @Override + public synchronized void resetData() { + datas.clear(); + unlockIncrement(); + } + + @Override + public long getDataSizeWithoutDuplicate() { + return getAllDataWithoutDuplicate().size(); + } + + @Override + public synchronized void addData(Object data) { + if (lock) { + return; + } + datas.add(data); + } + + @Override + public long getDataSize() { + return datas.size(); + } + + @Override + public boolean isRepeatedData(Object data) { + return Collections.frequency(datas, data) == 1; + } + + @Override + public synchronized Collection getAllDataWithoutDuplicate() { + return new HashSet(datas); + } + + @Override + public int getRepeatedTimeForData(Object data) { + int res = 0; + for (Object obj : datas) { + if (obj.equals(data)) { + res++; + } + } + return res; + } + + @Override + public synchronized void removeData(Object data) { + datas.remove(data); + } + + @Override + public void lockIncrement() { + lock = true; + } + + @Override + public void unlockIncrement() { + lock = false; + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/util/data/collect/impl/MapDataCollectorImpl.java b/test/src/main/java/org/apache/rocketmq/test/util/data/collect/impl/MapDataCollectorImpl.java new file mode 100644 index 0000000..7c51af7 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/util/data/collect/impl/MapDataCollectorImpl.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.test.util.data.collect.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.rocketmq.test.util.data.collect.DataCollector; + +public class MapDataCollectorImpl implements DataCollector { + + private Map datas = new ConcurrentHashMap(); + private boolean lock = false; + + public MapDataCollectorImpl() { + + } + + public MapDataCollectorImpl(Collection datas) { + for (Object data : datas) { + addData(data); + } + } + + @Override + public synchronized void addData(Object data) { + if (lock) { + return; + } + if (datas.containsKey(data)) { + datas.get(data).addAndGet(1); + } else { + datas.put(data, new AtomicInteger(1)); + } + } + + @Override + public Collection getAllData() { + List lst = new ArrayList(); + for (Entry entry : datas.entrySet()) { + for (int i = 0; i < entry.getValue().get(); i++) { + lst.add(entry.getKey()); + } + } + return lst; + } + + @Override + public long getDataSizeWithoutDuplicate() { + return datas.keySet().size(); + } + + @Override + public void resetData() { + datas.clear(); + unlockIncrement(); + } + + @Override + public long getDataSize() { + long sum = 0; + for (AtomicInteger count : datas.values()) { + sum = sum + count.get(); + } + return sum; + } + + @Override + public boolean isRepeatedData(Object data) { + if (datas.containsKey(data)) { + return datas.get(data).get() == 1; + } + return false; + } + + @Override + public Collection getAllDataWithoutDuplicate() { + return datas.keySet(); + } + + @Override + public int getRepeatedTimeForData(Object data) { + if (datas.containsKey(data)) { + return datas.get(data).intValue(); + } + return 0; + } + + @Override + public void removeData(Object data) { + datas.remove(data); + } + + @Override + public void lockIncrement() { + lock = true; + } + + @Override + public void unlockIncrement() { + lock = false; + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/util/parallel/ParallelTask.java b/test/src/main/java/org/apache/rocketmq/test/util/parallel/ParallelTask.java new file mode 100644 index 0000000..a4ad9a8 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/util/parallel/ParallelTask.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.test.util.parallel; + +import java.util.concurrent.CountDownLatch; + +public abstract class ParallelTask extends Thread { + private CountDownLatch latch = null; + + public CountDownLatch getLatch() { + return latch; + } + + public void setLatch(CountDownLatch latch) { + this.latch = latch; + } + + public abstract void execute(); + + @Override + public void run() { + this.execute(); + + if (latch != null) { + latch.countDown(); + } + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/util/parallel/ParallelTaskExecutor.java b/test/src/main/java/org/apache/rocketmq/test/util/parallel/ParallelTaskExecutor.java new file mode 100644 index 0000000..e7e9209 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/util/parallel/ParallelTaskExecutor.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.test.util.parallel; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class ParallelTaskExecutor { + public List tasks = new ArrayList(); + public ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); + public CountDownLatch latch = null; + + public ParallelTaskExecutor() { + + } + + public void pushTask(ParallelTask task) { + tasks.add(task); + } + + public void startBlock() { + init(); + startTask(); + try { + latch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public void startNoBlock() { + for (ParallelTask task : tasks) { + cachedThreadPool.execute(task); + } + } + + private void init() { + latch = new CountDownLatch(tasks.size()); + for (ParallelTask task : tasks) { + task.setLatch(latch); + } + } + + private void startTask() { + for (ParallelTask task : tasks) { + task.start(); + } + } +} diff --git a/test/src/main/java/org/apache/rocketmq/test/util/parallel/Task4Test.java b/test/src/main/java/org/apache/rocketmq/test/util/parallel/Task4Test.java new file mode 100644 index 0000000..c168d66 --- /dev/null +++ b/test/src/main/java/org/apache/rocketmq/test/util/parallel/Task4Test.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.test.util.parallel; + +public class Task4Test extends ParallelTask { + private String name = ""; + + public Task4Test(String name) { + this.name = name; + } + + @Override + public void execute() { + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/autoswitchrole/AutoSwitchRoleBase.java b/test/src/test/java/org/apache/rocketmq/test/autoswitchrole/AutoSwitchRoleBase.java new file mode 100644 index 0000000..4619596 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/autoswitchrole/AutoSwitchRoleBase.java @@ -0,0 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.test.autoswitchrole; + +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.ControllerConfig; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.GetMessageStatus; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.config.FlushDiskType; +import org.apache.rocketmq.store.config.MessageStoreConfig; + +import static org.awaitility.Awaitility.await; + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class AutoSwitchRoleBase { + + protected static final String STORE_PATH_ROOT_PARENT_DIR = System.getProperty("user.home") + File.separator + + UUID.randomUUID().toString().replace("-", ""); + private static final String STORE_PATH_ROOT_DIR = STORE_PATH_ROOT_PARENT_DIR + File.separator + "store"; + private static final String STORE_MESSAGE = "Once, there was a chance for me!"; + private static final byte[] MESSAGE_BODY = STORE_MESSAGE.getBytes(); + protected static List brokerList; + private static SocketAddress bornHost; + private static SocketAddress storeHost; + private static int number = 0; + + protected static void initialize() { + brokerList = new ArrayList<>(); + try { + storeHost = new InetSocketAddress(InetAddress.getLocalHost(), 8123); + bornHost = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0); + } catch (Exception ignored) { + } + } + + public static int nextPort() throws IOException { + return nextPort(1001, 9999); + } + + public static int nextPort(int minPort, int maxPort) throws IOException { + + Random random = new Random(); + int tempPort; + int port; + while (true) { + try { + tempPort = random.nextInt(maxPort) % (maxPort - minPort + 1) + minPort; + ServerSocket serverSocket = new ServerSocket(tempPort); + port = serverSocket.getLocalPort(); + serverSocket.close(); + break; + } catch (IOException ignored) { + if (number > 200) { + throw new IOException("This server's open ports are temporarily full!"); + } + ++number; + } + } + number = 0; + return port; + } + + public BrokerController startBroker(String namesrvAddress, String controllerAddress, String brokerName, + int brokerId, int haPort, + int brokerListenPort, + int nettyListenPort, BrokerRole expectedRole, int mappedFileSize) throws Exception { + final MessageStoreConfig storeConfig = buildMessageStoreConfig(brokerName + "#" + brokerId, haPort, mappedFileSize); + storeConfig.setHaMaxTimeSlaveNotCatchup(3 * 1000); + final BrokerConfig brokerConfig = new BrokerConfig(); + brokerConfig.setListenPort(brokerListenPort); + brokerConfig.setNamesrvAddr(namesrvAddress); + brokerConfig.setControllerAddr(controllerAddress); + brokerConfig.setSyncBrokerMetadataPeriod(2 * 1000); + brokerConfig.setCheckSyncStateSetPeriod(2 * 1000); + brokerConfig.setBrokerName(brokerName); + brokerConfig.setEnableControllerMode(true); + + final NettyServerConfig nettyServerConfig = new NettyServerConfig(); + nettyServerConfig.setListenPort(nettyListenPort); + + final BrokerController brokerController = new BrokerController(brokerConfig, nettyServerConfig, new NettyClientConfig(), storeConfig); + assertTrue(brokerController.initialize()); + brokerController.start(); + brokerList.add(brokerController); + await().atMost(20, TimeUnit.SECONDS).until(() -> (expectedRole == BrokerRole.SYNC_MASTER) == brokerController.getReplicasManager().isMasterState()); + return brokerController; + } + + protected MessageStoreConfig buildMessageStoreConfig(final String brokerDir, final int haPort, + final int mappedFileSize) { + MessageStoreConfig storeConfig = new MessageStoreConfig(); + storeConfig.setHaSendHeartbeatInterval(1000); + storeConfig.setBrokerRole(BrokerRole.SLAVE); + storeConfig.setHaListenPort(haPort); + storeConfig.setStorePathRootDir(STORE_PATH_ROOT_DIR + File.separator + brokerDir); + storeConfig.setStorePathCommitLog(STORE_PATH_ROOT_DIR + File.separator + brokerDir + File.separator + "commitlog"); + storeConfig.setStorePathEpochFile(STORE_PATH_ROOT_DIR + File.separator + brokerDir + File.separator + "EpochFileCache"); + storeConfig.setStorePathBrokerIdentity(STORE_PATH_ROOT_DIR + File.separator + brokerDir + File.separator + "brokerIdentity"); + storeConfig.setTotalReplicas(3); + storeConfig.setInSyncReplicas(2); + + storeConfig.setMappedFileSizeCommitLog(mappedFileSize); + storeConfig.setMappedFileSizeConsumeQueue(1024 * 1024); + storeConfig.setMaxHashSlotNum(10000); + storeConfig.setMaxIndexNum(100 * 100); + storeConfig.setFlushDiskType(FlushDiskType.SYNC_FLUSH); + storeConfig.setFlushIntervalConsumeQueue(1); + return storeConfig; + } + + protected static ControllerConfig buildControllerConfig(final String id, final String peers) { + final ControllerConfig config = new ControllerConfig(); + config.setControllerDLegerGroup("group1"); + config.setControllerDLegerPeers(peers); + config.setControllerDLegerSelfId(id); + config.setMappedFileSize(1024 * 1024); + config.setControllerStorePath(STORE_PATH_ROOT_DIR + File.separator + "namesrv" + id + File.separator + "DLedgerController"); + return config; + } + + protected MessageExtBrokerInner buildMessage(String topic) { + MessageExtBrokerInner msg = new MessageExtBrokerInner(); + msg.setTopic(topic); + msg.setTags("TAG1"); + msg.setBody(MESSAGE_BODY); + msg.setKeys(String.valueOf(System.currentTimeMillis())); + msg.setQueueId(0); + msg.setSysFlag(0); + msg.setBornTimestamp(System.currentTimeMillis()); + msg.setStoreHost(storeHost); + msg.setBornHost(bornHost); + msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); + return msg; + } + + protected void putMessage(MessageStore messageStore, String topic) { + // Put message on master + for (int i = 0; i < 10; i++) { + assertSame(messageStore.putMessage(buildMessage(topic)).getPutMessageStatus(), PutMessageStatus.PUT_OK); + } + } + + protected void checkMessage(final MessageStore messageStore, String topic, int totalNums, int startOffset) { + await().atMost(30, TimeUnit.SECONDS) + .until(() -> { + GetMessageResult result = messageStore.getMessage("GROUP_A", topic, 0, startOffset, 1024, null); +// System.out.printf(result + "%n"); +// System.out.printf("maxPhyOffset=" + messageStore.getMaxPhyOffset() + "%n"); +// System.out.printf("confirmOffset=" + messageStore.getConfirmOffset() + "%n"); + return result != null && result.getStatus() == GetMessageStatus.FOUND && result.getMessageCount() >= totalNums; + }); + } + +} diff --git a/test/src/test/java/org/apache/rocketmq/test/autoswitchrole/AutoSwitchRoleIntegrationTest.java b/test/src/test/java/org/apache/rocketmq/test/autoswitchrole/AutoSwitchRoleIntegrationTest.java new file mode 100644 index 0000000..0026307 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/autoswitchrole/AutoSwitchRoleIntegrationTest.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.test.autoswitchrole; + +import java.io.File; +import java.util.Random; +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.controller.ReplicasManager; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.ControllerConfig; +import org.apache.rocketmq.common.namesrv.NamesrvConfig; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.body.SyncStateSet; +import org.apache.rocketmq.controller.ControllerManager; +import org.apache.rocketmq.namesrv.NamesrvController; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.header.controller.GetReplicaInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetReplicaInfoResponseHeader; +import org.apache.rocketmq.store.MappedFileQueue; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.ha.HAClient; +import org.apache.rocketmq.store.ha.HAConnectionState; +import org.apache.rocketmq.store.ha.autoswitch.AutoSwitchHAService; +import org.apache.rocketmq.store.logfile.MappedFile; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@Ignore +public class AutoSwitchRoleIntegrationTest extends AutoSwitchRoleBase { + + private static final int DEFAULT_FILE_SIZE = 1024 * 1024; + private static NamesrvController namesrvController; + private static ControllerManager controllerManager; + private static String nameserverAddress; + private static String controllerAddress; + + private static ControllerConfig controllerConfig; + + private BrokerController brokerController1; + private BrokerController brokerController2; + private Random random = new Random(); + + @BeforeClass + public static void init() throws Exception { + initialize(); + + int controllerPort = nextPort(); + final String peers = String.format("n0-localhost:%d", controllerPort); + + final NettyServerConfig serverConfig = new NettyServerConfig(); + int namesrvPort = nextPort(); + serverConfig.setListenPort(namesrvPort); + + controllerConfig = buildControllerConfig("n0", peers); + namesrvController = new NamesrvController(new NamesrvConfig(), serverConfig, new NettyClientConfig()); + assertTrue(namesrvController.initialize()); + namesrvController.start(); + + initAndStartControllerManager(); + + nameserverAddress = "127.0.0.1:" + namesrvPort + ";"; + controllerAddress = "127.0.0.1:" + controllerPort + ";"; + } + + private static void initAndStartControllerManager() { + controllerManager = new ControllerManager(controllerConfig, new NettyServerConfig(), new NettyClientConfig()); + assertTrue(controllerManager.initialize()); + controllerManager.start(); + } + + public void initBroker(int mappedFileSize, String brokerName) throws Exception { + + this.brokerController1 = startBroker(nameserverAddress, controllerAddress, brokerName, 1, nextPort(), nextPort(), nextPort(), BrokerRole.SYNC_MASTER, mappedFileSize); + this.brokerController2 = startBroker(nameserverAddress, controllerAddress, brokerName, 2, nextPort(), nextPort(), nextPort(), BrokerRole.SLAVE, mappedFileSize); + // Wait slave connecting to master + assertTrue(waitSlaveReady(this.brokerController2.getMessageStore())); + Thread.sleep(1000); + } + + public void mockData(String topic) throws Exception { + final MessageStore messageStore = brokerController1.getMessageStore(); + putMessage(messageStore, topic); + // Check slave message + checkMessage(brokerController2.getMessageStore(), topic, 10, 0); + } + + public boolean waitSlaveReady(MessageStore messageStore) throws InterruptedException { + int tryTimes = 0; + while (tryTimes < 100) { + final HAClient haClient = messageStore.getHaService().getHAClient(); + if (haClient != null && haClient.getCurrentState().equals(HAConnectionState.TRANSFER)) { + return true; + } else { + Thread.sleep(2000); + tryTimes++; + } + } + return false; + } + + @Test + public void testCheckSyncStateSet() throws Exception { + String topic = "Topic-" + AutoSwitchRoleIntegrationTest.class.getSimpleName() + random.nextInt(65535); + String brokerName = "Broker-" + AutoSwitchRoleIntegrationTest.class.getSimpleName() + random.nextInt(65535); + initBroker(DEFAULT_FILE_SIZE, brokerName); + + mockData(topic); + + // Check SyncStateSet + final ReplicasManager replicasManager = brokerController1.getReplicasManager(); + SyncStateSet syncStateSet = replicasManager.getSyncStateSet(); + assertEquals(2, syncStateSet.getSyncStateSet().size()); + + // Shutdown controller2 + ScheduledExecutorService singleThread = Executors.newSingleThreadScheduledExecutor(); + while (!singleThread.awaitTermination(6 * 1000, TimeUnit.MILLISECONDS)) { + this.brokerController2.shutdown(); + singleThread.shutdown(); + } + + syncStateSet = replicasManager.getSyncStateSet(); + shutdownAndClearBroker(); + assertEquals(1, syncStateSet.getSyncStateSet().size()); + } + + @Test + public void testChangeMaster() throws Exception { + String topic = "Topic-" + AutoSwitchRoleIntegrationTest.class.getSimpleName() + random.nextInt(65535); + String brokerName = "Broker-" + AutoSwitchRoleIntegrationTest.class.getSimpleName() + random.nextInt(65535); + initBroker(DEFAULT_FILE_SIZE, brokerName); + int listenPort = brokerController1.getBrokerConfig().getListenPort(); + int nettyPort = brokerController1.getNettyServerConfig().getListenPort(); + mockData(topic); + + // Let master shutdown + brokerController1.shutdown(); + brokerList.remove(this.brokerController1); + Thread.sleep(6000); + + // The slave should change to master + assertTrue(brokerController2.getReplicasManager().isMasterState()); + assertEquals(brokerController2.getReplicasManager().getMasterEpoch(), 2); + + // Restart old master, it should be slave + brokerController1 = startBroker(nameserverAddress, controllerAddress, brokerName, 1, nextPort(), listenPort, nettyPort, BrokerRole.SLAVE, DEFAULT_FILE_SIZE); + waitSlaveReady(brokerController1.getMessageStore()); + + assertFalse(brokerController1.getReplicasManager().isMasterState()); + assertEquals(brokerController1.getReplicasManager().getMasterAddress(), brokerController2.getReplicasManager().getBrokerAddress()); + + // Put another batch messages + final MessageStore messageStore = brokerController2.getMessageStore(); + putMessage(messageStore, topic); + + // Check slave message + checkMessage(brokerController1.getMessageStore(), topic, 20, 0); + shutdownAndClearBroker(); + } + + + @Test + public void testRestartWithChangedAddress() throws Exception { + String topic = "Topic-" + AutoSwitchRoleIntegrationTest.class.getSimpleName() + random.nextInt(65535); + String brokerName = "Broker-" + AutoSwitchRoleIntegrationTest.class.getSimpleName() + random.nextInt(65535); + int oldPort = nextPort(); + this.brokerController1 = startBroker(nameserverAddress, controllerAddress, brokerName, 1, nextPort(), oldPort, oldPort, BrokerRole.SYNC_MASTER, DEFAULT_FILE_SIZE); + Thread.sleep(1000); + assertTrue(brokerController1.getReplicasManager().isMasterState()); + assertEquals(brokerController1.getReplicasManager().getMasterEpoch(), 1); + + // Let master shutdown + brokerController1.shutdown(); + brokerList.remove(this.brokerController1); + Thread.sleep(6000); + + // Restart with changed address + int newPort = nextPort(); + this.brokerController1 = startBroker(nameserverAddress, controllerAddress, brokerName, 1, nextPort(), newPort, newPort, BrokerRole.SYNC_MASTER, DEFAULT_FILE_SIZE); + Thread.sleep(1000); + + // Check broker id + assertEquals(1, brokerController1.getReplicasManager().getBrokerControllerId().longValue()); + // Check role + assertTrue(brokerController1.getReplicasManager().isMasterState()); + + // check ip address + RemotingCommand remotingCommand = controllerManager.getController().getReplicaInfo(new GetReplicaInfoRequestHeader(brokerName)).get(500, TimeUnit.MILLISECONDS); + GetReplicaInfoResponseHeader resp = (GetReplicaInfoResponseHeader) remotingCommand.readCustomHeader(); + assertEquals(1, resp.getMasterBrokerId().longValue()); + assertTrue(resp.getMasterAddress().contains(String.valueOf(newPort))); + shutdownAndClearBroker(); + } + + @Test + public void testBasicWorkWhenControllerShutdown() throws Exception { + String topic = "Foobar"; + String brokerName = "Broker-" + AutoSwitchRoleIntegrationTest.class.getSimpleName() + random.nextInt(); + initBroker(DEFAULT_FILE_SIZE, brokerName); + // Put message from 0 to 9 + putMessage(this.brokerController1.getMessageStore(), topic); + checkMessage(this.brokerController2.getMessageStore(), topic, 10, 0); + + // Shutdown Controller + controllerManager.shutdown(); + + // Put message from 10 to 19 + putMessage(this.brokerController1.getMessageStore(), topic); + checkMessage(this.brokerController2.getMessageStore(), topic, 20, 0); + + initAndStartControllerManager(); + } + + @Test + public void testAddBroker() throws Exception { + String topic = "Topic-" + AutoSwitchRoleIntegrationTest.class.getSimpleName() + random.nextInt(65535); + String brokerName = "Broker-" + AutoSwitchRoleIntegrationTest.class.getSimpleName() + random.nextInt(65535); + initBroker(DEFAULT_FILE_SIZE, brokerName); + mockData(topic); + + BrokerController broker3 = startBroker(nameserverAddress, controllerAddress, brokerName, 3, nextPort(), nextPort(), nextPort(), BrokerRole.SLAVE, DEFAULT_FILE_SIZE); + waitSlaveReady(broker3.getMessageStore()); + checkMessage(broker3.getMessageStore(), topic, 10, 0); + putMessage(this.brokerController1.getMessageStore(), topic); + checkMessage(broker3.getMessageStore(), topic, 20, 0); + shutdownAndClearBroker(); + } + + @Test + public void testTruncateEpochLogAndChangeMaster() throws Exception { + shutdownAndClearBroker(); + String topic = "FooBar"; + String brokerName = "Broker-" + AutoSwitchRoleIntegrationTest.class.getSimpleName() + random.nextInt(65535); + // Noted that 10 msg 's total size = 1570, and if init the mappedFileSize = 1700, one file only be used to store 10 msg. + initBroker(1700, brokerName); + // Step1: Put message + putMessage(this.brokerController1.getMessageStore(), topic); + checkMessage(this.brokerController2.getMessageStore(), topic, 10, 0); + + // Step2: shutdown broker1, broker2 as master + brokerController1.shutdown(); + brokerList.remove(brokerController1); + Thread.sleep(5000); + + assertTrue(brokerController2.getReplicasManager().isMasterState()); + assertEquals(brokerController2.getReplicasManager().getMasterEpoch(), 2); + + // Step3: add broker3 + BrokerController broker3 = startBroker(nameserverAddress, controllerAddress, brokerName, 3, nextPort(), nextPort(), nextPort(), BrokerRole.SLAVE, 1700); + waitSlaveReady(broker3.getMessageStore()); + checkMessage(broker3.getMessageStore(), topic, 10, 0); + + // Step4: put another batch message + // Master: + putMessage(this.brokerController2.getMessageStore(), topic); + checkMessage(broker3.getMessageStore(), topic, 20, 0); + + // Step5: 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 MessageStore broker2MessageStore = this.brokerController2.getMessageStore(); + final MappedFileQueue fileQueue = broker2MessageStore.getCommitLog().getMappedFileQueue(); + assertEquals(2, fileQueue.getTotalFileSize() / 1700); + + // Truncate epoch1's log (truncateEndOffset = 1570), which means we should delete the first file directly. + final MappedFile firstFile = broker2MessageStore.getCommitLog().getMappedFileQueue().getFirstMappedFile(); + firstFile.shutdown(1000); + fileQueue.retryDeleteFirstFile(1000); + assertEquals(broker2MessageStore.getCommitLog().getMinOffset(), 1700); + + final AutoSwitchHAService haService = (AutoSwitchHAService) this.brokerController2.getMessageStore().getHaService(); + haService.truncateEpochFilePrefix(1570); + checkMessage(broker2MessageStore, topic, 10, 10); + + // Step6, start broker4, link to broker2, it should sync msg from epoch2(offset = 1700). + BrokerController broker4 = startBroker(nameserverAddress, controllerAddress, brokerName, 4, nextPort(), nextPort(), nextPort(), BrokerRole.SLAVE, 1700); + waitSlaveReady(broker4.getMessageStore()); + checkMessage(broker4.getMessageStore(), topic, 10, 10); + shutdownAndClearBroker(); + } + + public void shutdownAndClearBroker() throws InterruptedException { + for (BrokerController controller : brokerList) { + controller.shutdown(); + UtilAll.deleteFile(new File(controller.getMessageStoreConfig().getStorePathRootDir())); + } + brokerList.clear(); + } + + @AfterClass + public static void destroy() { + if (namesrvController != null) { + namesrvController.shutdown(); + } + if (controllerManager != null) { + controllerManager.shutdown(); + } + File file = new File(STORE_PATH_ROOT_PARENT_DIR); + UtilAll.deleteFile(file); + } + +} diff --git a/test/src/test/java/org/apache/rocketmq/test/base/BaseConf.java b/test/src/test/java/org/apache/rocketmq/test/base/BaseConf.java new file mode 100644 index 0000000..472e106 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/base/BaseConf.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.test.base; + +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.client.consumer.MQPullConsumer; +import org.apache.rocketmq.client.consumer.MQPushConsumer; +import org.apache.rocketmq.client.producer.MQProducer; +import org.apache.rocketmq.client.producer.TransactionListener; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.common.attribute.CQType; +import org.apache.rocketmq.common.attribute.TopicMessageType; +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.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.test.client.rmq.RMQAsyncSendProducer; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.client.rmq.RMQTransactionalProducer; +import org.apache.rocketmq.test.clientinterface.AbstractMQConsumer; +import org.apache.rocketmq.test.clientinterface.AbstractMQProducer; +import org.apache.rocketmq.test.clientinterface.MQConsumer; +import org.apache.rocketmq.test.factory.ConsumerFactory; +import org.apache.rocketmq.test.listener.AbstractListener; +import org.apache.rocketmq.test.util.MQAdminTestUtils; +import org.apache.rocketmq.test.util.MQRandomUtils; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.admin.MQAdminExt; +import org.junit.Assert; + +import static org.apache.rocketmq.test.base.IntegrationTestBase.initMQAdmin; +import static org.awaitility.Awaitility.await; + +public class BaseConf { + + private final static Logger log = LoggerFactory.getLogger(BaseConf.class); + + public final static String NAMESRV_ADDR; + + //the logic queue test need at least three brokers + protected final static String CLUSTER_NAME; + protected final static String BROKER1_NAME; + protected final static String BROKER2_NAME; + protected final static String BROKER3_NAME; + + protected final static int BROKER_NUM = 3; + protected final static int WAIT_TIME = 5; + protected final static int CONSUME_TIME = 2 * 60 * 1000; + protected final static int QUEUE_NUMBERS = 8; + + protected static NamesrvController namesrvController; + protected static BrokerController brokerController1; + protected static BrokerController brokerController2; + protected static BrokerController brokerController3; + protected static List brokerControllerList; + protected static Map brokerControllerMap; + + protected static List mqClients = new ArrayList(); + protected static boolean debug = false; + + static { + System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION)); + namesrvController = IntegrationTestBase.createAndStartNamesrv(); + NAMESRV_ADDR = "127.0.0.1:" + namesrvController.getNettyServerConfig().getListenPort(); + log.debug("Name server started, listening: {}", NAMESRV_ADDR); + + brokerController1 = IntegrationTestBase.createAndStartBroker(NAMESRV_ADDR); + log.debug("Broker {} started, listening: {}", brokerController1.getBrokerConfig().getBrokerName(), + brokerController1.getBrokerConfig().getListenPort()); + + brokerController2 = IntegrationTestBase.createAndStartBroker(NAMESRV_ADDR); + log.debug("Broker {} started, listening: {}", brokerController2.getBrokerConfig().getBrokerName(), + brokerController2.getBrokerConfig().getListenPort()); + + brokerController3 = IntegrationTestBase.createAndStartBroker(NAMESRV_ADDR); + log.debug("Broker {} started, listening: {}", brokerController3.getBrokerConfig().getBrokerName(), + brokerController3.getBrokerConfig().getListenPort()); + + CLUSTER_NAME = brokerController1.getBrokerConfig().getBrokerClusterName(); + BROKER1_NAME = brokerController1.getBrokerConfig().getBrokerName(); + BROKER2_NAME = brokerController2.getBrokerConfig().getBrokerName(); + BROKER3_NAME = brokerController3.getBrokerConfig().getBrokerName(); + brokerControllerList = ImmutableList.of(brokerController1, brokerController2, brokerController3); + brokerControllerMap = brokerControllerList.stream().collect( + Collectors.toMap(input -> input.getBrokerConfig().getBrokerName(), Function.identity())); + initMQAdmin(NAMESRV_ADDR); + } + + public BaseConf() { + // Add waitBrokerRegistered to BaseConf constructor to make it default for all subclasses. + waitBrokerRegistered(NAMESRV_ADDR, CLUSTER_NAME, BROKER_NUM); + } + + // This method can't be placed in the static block of BaseConf, which seems to lead to a strange dead lock. + public static void waitBrokerRegistered(final String nsAddr, final String clusterName, final int expectedBrokerNum) { + final DefaultMQAdminExt mqAdminExt = new DefaultMQAdminExt(500); + mqAdminExt.setNamesrvAddr(nsAddr); + try { + mqAdminExt.start(); + await().atMost(30, TimeUnit.SECONDS).until(() -> { + List brokerDatas; + try { + brokerDatas = mqAdminExt.examineTopicRouteInfo(clusterName).getBrokerDatas(); + } catch (Exception e) { + return false; + } + return brokerDatas.size() == expectedBrokerNum; + }); + for (BrokerController brokerController: brokerControllerList) { + brokerController.getBrokerOuterAPI().refreshMetadata(); + } + } catch (Exception e) { + log.error("init failed, please check BaseConf", e); + Assert.fail(e.getMessage()); + } + ForkJoinPool.commonPool().execute(mqAdminExt::shutdown); + } + + public boolean awaitDispatchMs(long timeMs) throws Exception { + long start = System.currentTimeMillis(); + while (System.currentTimeMillis() - start <= timeMs) { + boolean allOk = true; + for (BrokerController brokerController: brokerControllerList) { + if (brokerController.getMessageStore().dispatchBehindBytes() != 0) { + allOk = false; + break; + } + } + if (allOk) { + return true; + } + Thread.sleep(100); + } + return false; + } + + + public static String initTopic() { + String topic = MQRandomUtils.getRandomTopic(); + return initTopicWithName(topic); + } + + public static String initTopic(TopicMessageType topicMessageType) { + String topic = MQRandomUtils.getRandomTopic(); + return initTopicWithName(topic, topicMessageType); + } + + public static String initTopicOnSampleTopicBroker(String sampleTopic) { + String topic = MQRandomUtils.getRandomTopic(); + return initTopicOnSampleTopicBroker(topic, sampleTopic); + } + + public static String initTopicOnSampleTopicBroker(String sampleTopic, TopicMessageType topicMessageType) { + String topic = MQRandomUtils.getRandomTopic(); + return initTopicOnSampleTopicBroker(topic, sampleTopic, topicMessageType); + } + + public static String initTopicWithName(String topicName) { + IntegrationTestBase.initTopic(topicName, NAMESRV_ADDR, CLUSTER_NAME, CQType.SimpleCQ); + return topicName; + } + + public static String initTopicWithName(String topicName, TopicMessageType topicMessageType) { + IntegrationTestBase.initTopic(topicName, NAMESRV_ADDR, CLUSTER_NAME, topicMessageType); + return topicName; + } + + public static String initTopicOnSampleTopicBroker(String topicName, String sampleTopic) { + IntegrationTestBase.initTopic(topicName, NAMESRV_ADDR, sampleTopic, CQType.SimpleCQ); + return topicName; + } + + public static String initTopicOnSampleTopicBroker(String topicName, String sampleTopic, TopicMessageType topicMessageType) { + IntegrationTestBase.initTopic(topicName, NAMESRV_ADDR, sampleTopic, topicMessageType); + return topicName; + } + + public static String initConsumerGroup() { + String group = MQRandomUtils.getRandomConsumerGroup(); + return initConsumerGroup(group); + } + + public static String initConsumerGroup(String group) { + MQAdminTestUtils.createSub(NAMESRV_ADDR, CLUSTER_NAME, group); + return group; + } + + public static DefaultMQAdminExt getAdmin(String nsAddr) { + final DefaultMQAdminExt mqAdminExt = new DefaultMQAdminExt(3 * 1000); + mqAdminExt.setNamesrvAddr(nsAddr); + mqAdminExt.setPollNameServerInterval(100); + mqClients.add(mqAdminExt); + return mqAdminExt; + } + + + public static RMQNormalProducer getProducer(String nsAddr, String topic) { + return getProducer(nsAddr, topic, false); + } + + public static RMQNormalProducer getProducer(String nsAddr, String topic, boolean useTLS) { + RMQNormalProducer producer = new RMQNormalProducer(nsAddr, topic, useTLS); + if (debug) { + producer.setDebug(); + } + mqClients.add(producer); + return producer; + } + + public static RMQTransactionalProducer getTransactionalProducer(String nsAddr, String topic, TransactionListener transactionListener) { + RMQTransactionalProducer producer = new RMQTransactionalProducer(nsAddr, topic, false, transactionListener); + if (debug) { + producer.setDebug(); + } + mqClients.add(producer); + return producer; + } + + public static RMQNormalProducer getProducer(String nsAddr, String topic, String producerGoup, + String instanceName) { + RMQNormalProducer producer = new RMQNormalProducer(nsAddr, topic, producerGoup, + instanceName); + if (debug) { + producer.setDebug(); + } + mqClients.add(producer); + return producer; + } + + public static RMQAsyncSendProducer getAsyncProducer(String nsAddr, String topic) { + RMQAsyncSendProducer producer = new RMQAsyncSendProducer(nsAddr, topic); + if (debug) { + producer.setDebug(); + } + mqClients.add(producer); + return producer; + } + + public static RMQNormalConsumer getConsumer(String nsAddr, String topic, String subExpression, + AbstractListener listener) { + return getConsumer(nsAddr, topic, subExpression, listener, false); + } + + public static RMQNormalConsumer getConsumer(String nsAddr, String topic, String subExpression, + AbstractListener listener, boolean useTLS) { + String consumerGroup = initConsumerGroup(); + return getConsumer(nsAddr, consumerGroup, topic, subExpression, listener, useTLS); + } + + public static RMQNormalConsumer getConsumer(String nsAddr, String consumerGroup, String topic, + String subExpression, AbstractListener listener) { + return getConsumer(nsAddr, consumerGroup, topic, subExpression, listener, false); + } + + public static RMQNormalConsumer getConsumer(String nsAddr, String consumerGroup, String topic, + String subExpression, AbstractListener listener, boolean useTLS) { + RMQNormalConsumer consumer = ConsumerFactory.getRMQNormalConsumer(nsAddr, consumerGroup, + topic, subExpression, listener, useTLS); + if (debug) { + consumer.setDebug(); + } + mqClients.add(consumer); + log.info("consumer[{}] start,topic[{}],subExpression[{}]", consumerGroup, topic, subExpression); + return consumer; + } + + public static void shutdown() { + ImmutableList mqClients = ImmutableList.copyOf(BaseConf.mqClients); + BaseConf.mqClients.clear(); + shutdown(mqClients); + } + + public static Set getBrokers() { + Set brokers = new HashSet<>(); + brokers.add(BROKER1_NAME); + brokers.add(BROKER2_NAME); + brokers.add(BROKER3_NAME); + return brokers; + } + + public static void shutdown(List mqClients) { + mqClients.forEach(mqClient -> ForkJoinPool.commonPool().execute(() -> { + if (mqClient instanceof AbstractMQProducer) { + ((AbstractMQProducer) mqClient).shutdown(); + } else if (mqClient instanceof AbstractMQConsumer) { + ((AbstractMQConsumer) mqClient).shutdown(); + } else if (mqClient instanceof MQAdminExt) { + ((MQAdminExt) mqClient).shutdown(); + } else if (mqClient instanceof MQProducer) { + ((MQProducer) mqClient).shutdown(); + } else if (mqClient instanceof MQPullConsumer) { + ((MQPullConsumer) mqClient).shutdown(); + } else if (mqClient instanceof MQPushConsumer) { + ((MQPushConsumer) mqClient).shutdown(); + } else if (mqClient instanceof MQConsumer) { + ((MQConsumer) mqClient).shutdown(); + } + })); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/base/IntegrationTestBase.java b/test/src/test/java/org/apache/rocketmq/test/base/IntegrationTestBase.java new file mode 100644 index 0000000..287e54d --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/base/IntegrationTestBase.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.test.base; + +import com.google.common.truth.Truth; +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.TopicAttributes; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.attribute.CQType; +import org.apache.rocketmq.common.attribute.TopicMessageType; +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.namesrv.NamesrvController; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.test.util.MQAdminTestUtils; + +public class IntegrationTestBase { + public static Logger logger = LoggerFactory.getLogger(IntegrationTestBase.class); + + protected static final String SEP = File.separator; + protected static final String BROKER_NAME_PREFIX = "TestBrokerName_"; + protected static final AtomicInteger BROKER_INDEX = new AtomicInteger(0); + protected static final List TMPE_FILES = new ArrayList<>(); + protected static final List BROKER_CONTROLLERS = new ArrayList<>(); + protected static final List NAMESRV_CONTROLLERS = new ArrayList<>(); + protected static int topicCreateTime = (int) TimeUnit.SECONDS.toSeconds(30); + public static volatile int commitLogSize = 1024 * 1024 * 100; + protected static final int INDEX_NUM = 1000; + + static { + + System.setProperty("rocketmq.client.logRoot", System.getProperty("java.io.tmpdir")); + + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + try { + for (BrokerController brokerController : BROKER_CONTROLLERS) { + if (brokerController != null) { + brokerController.shutdown(); + } + } + + // should destroy message store, otherwise could not delete the temp files. + for (BrokerController brokerController : BROKER_CONTROLLERS) { + if (brokerController != null) { + brokerController.getMessageStore().destroy(); + } + } + + for (NamesrvController namesrvController : NAMESRV_CONTROLLERS) { + if (namesrvController != null) { + namesrvController.shutdown(); + } + } + for (File file : TMPE_FILES) { + UtilAll.deleteFile(file); + } + MQAdminTestUtils.shutdownAdmin(); + } catch (Exception e) { + logger.error("Shutdown error", e); + } + } + }); + + } + + public static String createBaseDir() { + String baseDir = System.getProperty("java.io.tmpdir") + SEP + "unitteststore-" + UUID.randomUUID(); + final File file = new File(baseDir); + if (file.exists()) { + logger.info(String.format("[%s] has already existed, please back up and remove it for integration tests", baseDir)); + System.exit(1); + } + TMPE_FILES.add(file); + return baseDir; + } + + public static NamesrvController createAndStartNamesrv() { + String baseDir = createBaseDir(); + NamesrvConfig namesrvConfig = new NamesrvConfig(); + NettyServerConfig nameServerNettyServerConfig = new NettyServerConfig(); + namesrvConfig.setKvConfigPath(baseDir + SEP + "namesrv" + SEP + "kvConfig.json"); + namesrvConfig.setConfigStorePath(baseDir + SEP + "namesrv" + SEP + "namesrv.properties"); + + nameServerNettyServerConfig.setListenPort(0); + NamesrvController namesrvController = new NamesrvController(namesrvConfig, nameServerNettyServerConfig); + try { + Truth.assertThat(namesrvController.initialize()).isTrue(); + logger.info("Name Server Start:{}", nameServerNettyServerConfig.getListenPort()); + namesrvController.start(); + } catch (Exception e) { + logger.info("Name Server start failed", e); + System.exit(1); + } + NAMESRV_CONTROLLERS.add(namesrvController); + return namesrvController; + + } + + public static BrokerController createAndStartBroker(String nsAddr) { + String baseDir = createBaseDir(); + BrokerConfig brokerConfig = new BrokerConfig(); + MessageStoreConfig storeConfig = new MessageStoreConfig(); + brokerConfig.setBrokerName(BROKER_NAME_PREFIX + BROKER_INDEX.incrementAndGet()); + brokerConfig.setBrokerIP1("127.0.0.1"); + brokerConfig.setNamesrvAddr(nsAddr); + brokerConfig.setEnablePropertyFilter(true); + brokerConfig.setEnableCalcFilterBitMap(true); + brokerConfig.setAppendAckAsync(true); + brokerConfig.setAppendCkAsync(true); + brokerConfig.setRecallMessageEnable(true); + storeConfig.setEnableConsumeQueueExt(true); + brokerConfig.setLoadBalancePollNameServerInterval(500); + storeConfig.setStorePathRootDir(baseDir); + storeConfig.setStorePathCommitLog(baseDir + SEP + "commitlog"); + storeConfig.setMappedFileSizeCommitLog(commitLogSize); + storeConfig.setMaxIndexNum(INDEX_NUM); + storeConfig.setMaxHashSlotNum(INDEX_NUM * 4); + storeConfig.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"); + storeConfig.setMaxTransferCountOnMessageInMemory(1024); + storeConfig.setMaxTransferCountOnMessageInDisk(1024); + return createAndStartBroker(storeConfig, brokerConfig); + } + + public static BrokerController createAndStartBroker(MessageStoreConfig storeConfig, BrokerConfig brokerConfig) { + NettyServerConfig nettyServerConfig = new NettyServerConfig(); + NettyClientConfig nettyClientConfig = new NettyClientConfig(); + nettyServerConfig.setListenPort(0); + storeConfig.setHaListenPort(0); + BrokerController brokerController = new BrokerController(brokerConfig, nettyServerConfig, nettyClientConfig, storeConfig); + try { + Truth.assertThat(brokerController.initialize()).isTrue(); + logger.info("Broker Start name:{} addr:{}", brokerConfig.getBrokerName(), brokerController.getBrokerAddr()); + brokerController.start(); + } catch (Throwable t) { + logger.error("Broker start failed, will exit", t); + System.exit(1); + } + BROKER_CONTROLLERS.add(brokerController); + return brokerController; + } + + public static boolean initTopic(String topic, String nsAddr, String clusterName, int queueNumbers, CQType cqType) { + return initTopic(topic, nsAddr, clusterName, queueNumbers, cqType, TopicMessageType.NORMAL); + } + + public static boolean initTopic(String topic, String nsAddr, String clusterName, int queueNumbers, CQType cqType, TopicMessageType topicMessageType) { + boolean createResult; + Map attributes = new HashMap<>(); + if (!Objects.equals(CQType.SimpleCQ, cqType)) { + attributes.put("+" + TopicAttributes.QUEUE_TYPE_ATTRIBUTE.getName(), cqType.toString()); + } + if (!Objects.equals(TopicMessageType.NORMAL, topicMessageType)) { + attributes.put("+" + TopicAttributes.TOPIC_MESSAGE_TYPE_ATTRIBUTE.getName(), topicMessageType.toString()); + } + createResult = MQAdminTestUtils.createTopic(nsAddr, clusterName, topic, queueNumbers, attributes, topicCreateTime); + return createResult; + } + + public static boolean initTopic(String topic, String nsAddr, String clusterName, CQType cqType) { + return initTopic(topic, nsAddr, clusterName, BaseConf.QUEUE_NUMBERS, cqType, TopicMessageType.NORMAL); + } + + public static boolean initTopic(String topic, String nsAddr, String clusterName, TopicMessageType topicMessageType) { + return initTopic(topic, nsAddr, clusterName, BaseConf.QUEUE_NUMBERS, CQType.SimpleCQ, topicMessageType); + } + + public static void deleteFile(File file) { + if (!file.exists()) { + return; + } + UtilAll.deleteFile(file); + } + + public static void initMQAdmin(String nsAddr) { + try { + MQAdminTestUtils.startAdmin(nsAddr); + } catch (MQClientException e) { + logger.info("MQAdmin start failed"); + System.exit(1); + } + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/balance/NormalMsgDynamicBalanceIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/balance/NormalMsgDynamicBalanceIT.java new file mode 100644 index 0000000..7408a09 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/balance/NormalMsgDynamicBalanceIT.java @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.test.client.consumer.balance; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.MQWait; +import org.apache.rocketmq.test.util.TestUtils; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class NormalMsgDynamicBalanceIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(NormalMsgStaticBalanceIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("use topic: %s !", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testTwoConsumerAndCrashOne() { + int msgSize = 400; + RMQNormalConsumer consumer1 = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + RMQNormalConsumer consumer2 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), topic, + "*", new RMQNormalListener()); + TestUtils.waitForSeconds(WAIT_TIME); + + producer.send(msgSize); + + MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), consumer1.getListener(), + consumer2.getListener()); + consumer2.shutdown(); + + producer.send(msgSize); + Assert.assertEquals("Not all are sent", msgSize * 2, producer.getAllUndupMsgBody().size()); + + boolean recvAll = MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), + consumer1.getListener(), consumer2.getListener()); + assertThat(recvAll).isEqualTo(true); + + boolean balance = VerifyUtils.verifyBalance(msgSize, + VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer1.getListener().getAllUndupMsgBody()).size() - msgSize, + VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer2.getListener().getAllUndupMsgBody()).size()); + assertThat(balance).isEqualTo(true); + } + + @Test + public void test3ConsumerAndCrashOne() { + int msgSize = 400; + RMQNormalConsumer consumer1 = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + RMQNormalConsumer consumer2 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), topic, + "*", new RMQNormalListener()); + RMQNormalConsumer consumer3 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), topic, + "*", new RMQNormalListener()); + TestUtils.waitForSeconds(WAIT_TIME); + + producer.send(msgSize); + + MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), consumer1.getListener(), + consumer2.getListener(), consumer3.getListener()); + consumer3.shutdown(); + TestUtils.waitForSeconds(WAIT_TIME); + + producer.clearMsg(); + consumer1.clearMsg(); + consumer2.clearMsg(); + + producer.send(msgSize); + Assert.assertEquals("Not all are sent", msgSize, producer.getAllUndupMsgBody().size()); + + boolean recvAll = MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), + consumer1.getListener(), consumer2.getListener()); + assertThat(recvAll).isEqualTo(true); + + boolean balance = VerifyUtils.verifyBalance(msgSize, + VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer1.getListener().getAllUndupMsgBody()).size(), + VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer2.getListener().getAllUndupMsgBody()).size()); + assertThat(balance).isEqualTo(true); + } + + @Test + public void testMessageQueueListener() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + + RMQNormalConsumer consumer1 = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + // Register message queue listener + consumer1.getConsumer().setMessageQueueListener((topic, mqAll, mqAssigned) -> latch.countDown()); + + // Without message queue listener + RMQNormalConsumer consumer2 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), topic, + "*", new RMQNormalListener()); + + Assert.assertTrue(latch.await(30, TimeUnit.SECONDS)); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/balance/NormalMsgStaticBalanceIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/balance/NormalMsgStaticBalanceIT.java new file mode 100644 index 0000000..7af23d5 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/balance/NormalMsgStaticBalanceIT.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.test.client.consumer.balance; + +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.MQWait; +import org.apache.rocketmq.test.util.TestUtils; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class NormalMsgStaticBalanceIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(NormalMsgStaticBalanceIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("use topic: %s !", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testTwoConsumersBalance() { + int msgSize = 400; + RMQNormalConsumer consumer1 = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + RMQNormalConsumer consumer2 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), topic, + "*", new RMQNormalListener()); + TestUtils.waitForSeconds(WAIT_TIME); + + producer.send(msgSize); + Assert.assertEquals("Not all are sent", msgSize, producer.getAllUndupMsgBody().size()); + + boolean recvAll = MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), + consumer1.getListener(), consumer2.getListener()); + assertThat(recvAll).isEqualTo(true); + + boolean balance = VerifyUtils.verifyBalance(msgSize, + VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer1.getListener().getAllUndupMsgBody()).size(), + VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer2.getListener().getAllUndupMsgBody()).size()); + assertThat(balance).isEqualTo(true); + } + + @Test + public void testFourConsumersBalance() { + int msgSize = 600; + String consumerGroup = initConsumerGroup(); + logger.info("use group: {}", consumerGroup); + RMQNormalConsumer consumer1 = getConsumer(NAMESRV_ADDR, consumerGroup, topic, "*", new RMQNormalListener()); + RMQNormalConsumer consumer2 = getConsumer(NAMESRV_ADDR, consumerGroup, topic, "*", new RMQNormalListener()); + RMQNormalConsumer consumer3 = getConsumer(NAMESRV_ADDR, consumerGroup, topic, "*", new RMQNormalListener()); + RMQNormalConsumer consumer4 = getConsumer(NAMESRV_ADDR, consumerGroup, topic, "*", new RMQNormalListener()); + TestUtils.waitForSeconds(WAIT_TIME); + + producer.send(msgSize); + Assert.assertEquals("Not all are sent", msgSize, producer.getAllUndupMsgBody().size()); + + boolean recvAll = MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), + consumer1.getListener(), consumer2.getListener(), consumer3.getListener(), + consumer4.getListener()); + assertThat(recvAll).isEqualTo(true); + + boolean balance = VerifyUtils + .verifyBalance(msgSize, + VerifyUtils + .getFilterdMessage(producer.getAllMsgBody(), + consumer1.getListener().getAllUndupMsgBody()) + .size(), + VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer2.getListener().getAllUndupMsgBody()).size(), + VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer3.getListener().getAllUndupMsgBody()).size(), + VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer4.getListener().getAllUndupMsgBody()).size()); + assertThat(balance).isEqualTo(true); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/BaseBroadcast.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/BaseBroadcast.java new file mode 100644 index 0000000..9b284e6 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/BaseBroadcast.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.test.client.consumer.broadcast; + +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQBroadCastConsumer; +import org.apache.rocketmq.test.factory.ConsumerFactory; +import org.apache.rocketmq.test.listener.AbstractListener; + +public class BaseBroadcast extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(BaseBroadcast.class); + + public static RMQBroadCastConsumer getBroadCastConsumer(String nsAddr, String topic, + String subExpression, + AbstractListener listener) { + String consumerGroup = initConsumerGroup(); + return getBroadCastConsumer(nsAddr, consumerGroup, topic, subExpression, listener); + } + + public static RMQBroadCastConsumer getBroadCastConsumer(String nsAddr, String consumerGroup, + String topic, String subExpression, + AbstractListener listener) { + RMQBroadCastConsumer consumer = ConsumerFactory.getRMQBroadCastConsumer(nsAddr, + consumerGroup, topic, subExpression, listener); + + consumer.setDebug(); + + mqClients.add(consumer); + logger.info(String.format("consumer[%s] start,topic[%s],subExpression[%s]", consumerGroup, + topic, subExpression)); + return consumer; + } + + public void printSeparator() { + for (int i = 0; i < 3; i++) { + logger.info( + "<<<<<<<<================================================================================>>>>>>>>"); + } + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/normal/BroadcastNormalMsgNotReceiveIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/normal/BroadcastNormalMsgNotReceiveIT.java new file mode 100644 index 0000000..a4af5f7 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/normal/BroadcastNormalMsgNotReceiveIT.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.test.client.consumer.broadcast.normal; + +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.client.consumer.broadcast.BaseBroadcast; +import org.apache.rocketmq.test.client.rmq.RMQBroadCastConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class BroadcastNormalMsgNotReceiveIT extends BaseBroadcast { + private static Logger logger = LoggerFactory + .getLogger(NormalMsgTwoSameGroupConsumerIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + printSeparator(); + topic = initTopic(); + logger.info(String.format("use topic: %s;", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testNotConsumeAfterConsume() throws Exception { + int msgSize = 16; + + String group = initConsumerGroup(); + RMQBroadCastConsumer consumer1 = getBroadCastConsumer(NAMESRV_ADDR, group, topic, "*", + new RMQNormalListener(group + "_1")); + Thread.sleep(3000); + producer.send(msgSize); + Assert.assertEquals("Not all sent succeeded", msgSize, producer.getAllUndupMsgBody().size()); + + consumer1.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer1.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + + RMQBroadCastConsumer consumer2 = getBroadCastConsumer(NAMESRV_ADDR, + consumer1.getConsumerGroup(), topic, "*", new RMQNormalListener(group + "_2")); + consumer2.getListener().waitForMessageConsume(producer.getAllMsgBody(), WAIT_TIME); + assertThat(consumer2.getListener().getAllMsgBody().size()).isEqualTo(0); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/normal/BroadcastNormalMsgRecvCrashIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/normal/BroadcastNormalMsgRecvCrashIT.java new file mode 100644 index 0000000..a33710e --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/normal/BroadcastNormalMsgRecvCrashIT.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.test.client.consumer.broadcast.normal; + +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.client.consumer.broadcast.BaseBroadcast; +import org.apache.rocketmq.test.client.rmq.RMQBroadCastConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.TestUtils; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class BroadcastNormalMsgRecvCrashIT extends BaseBroadcast { + private static Logger logger = LoggerFactory + .getLogger(NormalMsgTwoSameGroupConsumerIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + printSeparator(); + topic = initTopic(); + logger.info(String.format("use topic: %s;", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testStartTwoAndCrashOneLater() { + int msgSize = 16; + + String group = initConsumerGroup(); + RMQBroadCastConsumer consumer1 = getBroadCastConsumer(NAMESRV_ADDR, group, topic, "*", + new RMQNormalListener(group + "_1")); + RMQBroadCastConsumer consumer2 = getBroadCastConsumer(NAMESRV_ADDR, + consumer1.getConsumerGroup(), topic, "*", new RMQNormalListener(group + "_2")); + TestUtils.waitForSeconds(WAIT_TIME); + + producer.send(msgSize); + Assert.assertEquals("Not all sent succeeded", msgSize, producer.getAllUndupMsgBody().size()); + + consumer1.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + consumer2.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer1.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer2.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + + consumer2.shutdown(); + + producer.clearMsg(); + consumer1.clearMsg(); + + producer.send(msgSize); + Assert.assertEquals("Not all sent succeeded", msgSize, producer.getAllUndupMsgBody().size()); + + consumer1.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer1.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/normal/BroadcastNormalMsgRecvFailIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/normal/BroadcastNormalMsgRecvFailIT.java new file mode 100644 index 0000000..0805679 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/normal/BroadcastNormalMsgRecvFailIT.java @@ -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. + */ + +package org.apache.rocketmq.test.client.consumer.broadcast.normal; + +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.client.consumer.broadcast.BaseBroadcast; +import org.apache.rocketmq.test.client.rmq.RMQBroadCastConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class BroadcastNormalMsgRecvFailIT extends BaseBroadcast { + private static Logger logger = LoggerFactory + .getLogger(NormalMsgTwoSameGroupConsumerIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + printSeparator(); + topic = initTopic(); + logger.info(String.format("use topic: %s;", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Ignore + @Test + public void testStartTwoConsumerAndOneConsumerFail() { + int msgSize = 16; + + RMQBroadCastConsumer consumer1 = getBroadCastConsumer(NAMESRV_ADDR, topic, "*", + new RMQNormalListener()); + RMQBroadCastConsumer consumer2 = getBroadCastConsumer(NAMESRV_ADDR, + consumer1.getConsumerGroup(), topic, "*", + new RMQNormalListener(ConsumeConcurrentlyStatus.RECONSUME_LATER)); + + producer.send(msgSize); + Assert.assertEquals("Not all sent succeeded", msgSize, producer.getAllUndupMsgBody().size()); + + consumer1.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer1.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/normal/BroadcastNormalMsgRecvStartLaterIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/normal/BroadcastNormalMsgRecvStartLaterIT.java new file mode 100644 index 0000000..9aa471c --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/normal/BroadcastNormalMsgRecvStartLaterIT.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.test.client.consumer.broadcast.normal; + +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.client.consumer.broadcast.BaseBroadcast; +import org.apache.rocketmq.test.client.rmq.RMQBroadCastConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.TestUtils; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class BroadcastNormalMsgRecvStartLaterIT extends BaseBroadcast { + private static Logger logger = LoggerFactory + .getLogger(NormalMsgTwoSameGroupConsumerIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + printSeparator(); + topic = initTopic(); + logger.info(String.format("use topic: %s;", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testStartOneAndStartAnotherLater() { + int msgSize = 16; + + String group = initConsumerGroup(); + RMQBroadCastConsumer consumer1 = getBroadCastConsumer(NAMESRV_ADDR, group, topic, "*", + new RMQNormalListener(group + "_1")); + TestUtils.waitForSeconds(WAIT_TIME); + + producer.send(msgSize); + Assert.assertEquals("Not all sent succeeded", msgSize, producer.getAllUndupMsgBody().size()); + + consumer1.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer1.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + + producer.clearMsg(); + consumer1.clearMsg(); + + RMQBroadCastConsumer consumer2 = getBroadCastConsumer(NAMESRV_ADDR, + consumer1.getConsumerGroup(), topic, "*", new RMQNormalListener(group + "_2")); + TestUtils.waitForSeconds(WAIT_TIME); + producer.send(msgSize); + Assert.assertEquals("Not all sent succeeded", msgSize, producer.getAllUndupMsgBody().size()); + + consumer1.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + consumer2.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer1.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer2.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/normal/BroadcastNormalMsgTwoDiffGroupRecvIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/normal/BroadcastNormalMsgTwoDiffGroupRecvIT.java new file mode 100644 index 0000000..12f0c70 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/normal/BroadcastNormalMsgTwoDiffGroupRecvIT.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.test.client.consumer.broadcast.normal; + +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.client.consumer.broadcast.BaseBroadcast; +import org.apache.rocketmq.test.client.rmq.RMQBroadCastConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.TestUtils; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class BroadcastNormalMsgTwoDiffGroupRecvIT extends BaseBroadcast { + private static Logger logger = LoggerFactory + .getLogger(NormalMsgTwoSameGroupConsumerIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + printSeparator(); + topic = initTopic(); + logger.info(String.format("use topic: %s;", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testStartDiffSameGroupConsumer() { + int msgSize = 16; + + String group1 = initConsumerGroup(); + String group2 = initConsumerGroup(); + RMQBroadCastConsumer consumer1 = getBroadCastConsumer(NAMESRV_ADDR, group1, topic, "*", + new RMQNormalListener(group1 + "_1")); + RMQBroadCastConsumer consumer2 = getBroadCastConsumer(NAMESRV_ADDR, group2, topic, "*", + new RMQNormalListener(group2 + "_2")); + TestUtils.waitForSeconds(WAIT_TIME); + + producer.send(msgSize); + Assert.assertEquals("Not all sent succeeded", msgSize, producer.getAllUndupMsgBody().size()); + + consumer1.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + consumer2.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer1.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer2.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/normal/NormalMsgTwoSameGroupConsumerIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/normal/NormalMsgTwoSameGroupConsumerIT.java new file mode 100644 index 0000000..a18b5ee --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/normal/NormalMsgTwoSameGroupConsumerIT.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.test.client.consumer.broadcast.normal; + +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.client.consumer.broadcast.BaseBroadcast; +import org.apache.rocketmq.test.client.rmq.RMQBroadCastConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.TestUtils; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class NormalMsgTwoSameGroupConsumerIT extends BaseBroadcast { + private static Logger logger = LoggerFactory + .getLogger(NormalMsgTwoSameGroupConsumerIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + printSeparator(); + topic = initTopic(); + logger.info(String.format("use topic: %s;", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testStartTwoSameGroupConsumer() { + int msgSize = 16; + + String group = initConsumerGroup(); + RMQBroadCastConsumer consumer1 = getBroadCastConsumer(NAMESRV_ADDR, group, topic, "*", + new RMQNormalListener(group + "_1")); + RMQBroadCastConsumer consumer2 = getBroadCastConsumer(NAMESRV_ADDR, + consumer1.getConsumerGroup(), topic, "*", new RMQNormalListener(group + "_2")); + TestUtils.waitForSeconds(WAIT_TIME); + + producer.send(msgSize); + Assert.assertEquals("Not all are sent", msgSize, producer.getAllUndupMsgBody().size()); + + consumer1.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + consumer2.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer1.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer2.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + } + +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/order/OrderMsgBroadcastIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/order/OrderMsgBroadcastIT.java new file mode 100644 index 0000000..d1e2c0d --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/order/OrderMsgBroadcastIT.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.test.client.consumer.broadcast.order; + +import java.util.List; +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.test.client.consumer.broadcast.BaseBroadcast; +import org.apache.rocketmq.test.client.rmq.RMQBroadCastConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.order.RMQOrderListener; +import org.apache.rocketmq.test.message.MessageQueueMsg; +import org.apache.rocketmq.test.util.TestUtils; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +/** + * Currently, does not support the ordered broadcast message + */ +@Ignore +public class OrderMsgBroadcastIT extends BaseBroadcast { + private static Logger logger = LoggerFactory.getLogger(OrderMsgBroadcastIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + + private int broadcastConsumeTime = 1 * 60 * 1000; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("use topic: %s;", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testTwoConsumerSubTag() { + int msgSize = 10; + + RMQBroadCastConsumer consumer1 = getBroadCastConsumer(NAMESRV_ADDR, topic, "*", + new RMQOrderListener()); + RMQBroadCastConsumer consumer2 = getBroadCastConsumer(NAMESRV_ADDR, + consumer1.getConsumerGroup(), topic, "*", new RMQOrderListener()); + TestUtils.waitForSeconds(WAIT_TIME); + + List mqs = producer.getMessageQueue(); + MessageQueueMsg mqMsgs = new MessageQueueMsg(mqs, msgSize); + producer.send(mqMsgs.getMsgsWithMQ()); + consumer1.getListener().waitForMessageConsume(producer.getAllMsgBody(), broadcastConsumeTime); + consumer2.getListener().waitForMessageConsume(producer.getAllMsgBody(), broadcastConsumeTime); + + assertThat(VerifyUtils.verifyOrder(((RMQOrderListener) consumer1.getListener()).getMsgs())) + .isEqualTo(true); + assertThat(VerifyUtils.verifyOrder(((RMQOrderListener) consumer2.getListener()).getMsgs())) + .isEqualTo(true); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/tag/BroadcastTwoConsumerFilterIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/tag/BroadcastTwoConsumerFilterIT.java new file mode 100644 index 0000000..6f96dcd --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/tag/BroadcastTwoConsumerFilterIT.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.test.client.consumer.broadcast.tag; + +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.client.consumer.broadcast.BaseBroadcast; +import org.apache.rocketmq.test.client.rmq.RMQBroadCastConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.TestUtils; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class BroadcastTwoConsumerFilterIT extends BaseBroadcast { + private static Logger logger = LoggerFactory.getLogger(BroadcastTwoConsumerSubTagIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("use topic: %s;", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testTwoConsumerFilter() { + int msgSize = 40; + String tag1 = "jueyin_tag_1"; + String tag2 = "jueyin_tag_2"; + + RMQBroadCastConsumer consumer1 = getBroadCastConsumer(NAMESRV_ADDR, topic, tag1, + new RMQNormalListener()); + RMQBroadCastConsumer consumer2 = getBroadCastConsumer(NAMESRV_ADDR, + consumer1.getConsumerGroup(), topic, tag1, new RMQNormalListener()); + TestUtils.waitForSeconds(WAIT_TIME); + + producer.send(tag2, msgSize); + Assert.assertEquals("Not all sent succeeded", msgSize, producer.getAllUndupMsgBody().size()); + producer.clearMsg(); + producer.send(tag1, msgSize); + + consumer1.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + consumer2.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer1.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer2.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/tag/BroadcastTwoConsumerSubDiffTagIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/tag/BroadcastTwoConsumerSubDiffTagIT.java new file mode 100644 index 0000000..6927f7f --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/tag/BroadcastTwoConsumerSubDiffTagIT.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.test.client.consumer.broadcast.tag; + +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.client.consumer.broadcast.BaseBroadcast; +import org.apache.rocketmq.test.client.rmq.RMQBroadCastConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.TestUtils; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class BroadcastTwoConsumerSubDiffTagIT extends BaseBroadcast { + private static Logger logger = LoggerFactory.getLogger(BroadcastTwoConsumerSubTagIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("use topic: %s;", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testTwoConsumerSubDiffTag() { + int msgSize = 40; + String tag = "jueyin_tag"; + + RMQBroadCastConsumer consumer1 = getBroadCastConsumer(NAMESRV_ADDR, topic, "*", + new RMQNormalListener()); + RMQBroadCastConsumer consumer2 = getBroadCastConsumer(NAMESRV_ADDR, + consumer1.getConsumerGroup(), topic, tag, new RMQNormalListener()); + TestUtils.waitForSeconds(WAIT_TIME); + + producer.send(tag, msgSize); + Assert.assertEquals("Not all sent succeeded", msgSize, producer.getAllUndupMsgBody().size()); + + consumer1.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + consumer2.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer1.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer2.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/tag/BroadcastTwoConsumerSubTagIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/tag/BroadcastTwoConsumerSubTagIT.java new file mode 100644 index 0000000..bd6d1cf --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/broadcast/tag/BroadcastTwoConsumerSubTagIT.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.test.client.consumer.broadcast.tag; + +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.client.consumer.broadcast.BaseBroadcast; +import org.apache.rocketmq.test.client.rmq.RMQBroadCastConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.TestUtils; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class BroadcastTwoConsumerSubTagIT extends BaseBroadcast { + private static Logger logger = LoggerFactory.getLogger(BroadcastTwoConsumerSubTagIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("use topic: %s;", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testTwoConsumerSubTag() { + int msgSize = 20; + String tag = "jueyin_tag"; + + RMQBroadCastConsumer consumer1 = getBroadCastConsumer(NAMESRV_ADDR, topic, tag, + new RMQNormalListener()); + RMQBroadCastConsumer consumer2 = getBroadCastConsumer(NAMESRV_ADDR, + consumer1.getConsumerGroup(), topic, tag, new RMQNormalListener()); + TestUtils.waitForSeconds(WAIT_TIME); + + producer.send(tag, msgSize); + Assert.assertEquals("Not all sent succeeded", msgSize, producer.getAllUndupMsgBody().size()); + + consumer1.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + consumer2.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer1.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer2.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/cluster/DynamicAddAndCrashIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/cluster/DynamicAddAndCrashIT.java new file mode 100644 index 0000000..d7603f1 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/cluster/DynamicAddAndCrashIT.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.test.client.consumer.cluster; + +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.consumer.balance.NormalMsgStaticBalanceIT; +import org.apache.rocketmq.test.client.mq.MQAsyncProducer; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.MQWait; +import org.apache.rocketmq.test.util.TestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class DynamicAddAndCrashIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(NormalMsgStaticBalanceIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("use topic: %s !", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testAddOneConsumerAndCrashAfterWhile() { + int msgSize = 150; + RMQNormalConsumer consumer1 = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + + MQAsyncProducer asyncDefaultMQProducer = new MQAsyncProducer(producer, msgSize, 100); + asyncDefaultMQProducer.start(); + TestUtils.waitForSeconds(WAIT_TIME); + + RMQNormalConsumer consumer2 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), topic, + "*", new RMQNormalListener()); + TestUtils.waitForSeconds(WAIT_TIME); + consumer2.shutdown(); + + asyncDefaultMQProducer.waitSendAll(WAIT_TIME * 6); + + MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), consumer1.getListener(), + consumer2.getListener()); + + boolean recvAll = MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), + consumer1.getListener(), consumer2.getListener()); + assertThat(recvAll).isEqualTo(true); + } + + @Test + public void testAddTwoConsumerAndCrashAfterWhile() { + int msgSize = 150; + RMQNormalConsumer consumer1 = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + + MQAsyncProducer asyncDefaultMQProducer = new MQAsyncProducer(producer, msgSize, 100); + asyncDefaultMQProducer.start(); + TestUtils.waitForSeconds(WAIT_TIME); + + RMQNormalConsumer consumer2 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), topic, + "*", new RMQNormalListener()); + RMQNormalConsumer consumer3 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), topic, + "*", new RMQNormalListener()); + TestUtils.waitForSeconds(WAIT_TIME); + + consumer2.shutdown(); + consumer3.shutdown(); + + asyncDefaultMQProducer.waitSendAll(WAIT_TIME * 6); + + MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), consumer1.getListener(), + consumer2.getListener(), consumer3.getListener()); + + boolean recvAll = MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), + consumer1.getListener(), consumer2.getListener(), consumer3.getListener()); + assertThat(recvAll).isEqualTo(true); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/cluster/DynamicAddConsumerIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/cluster/DynamicAddConsumerIT.java new file mode 100644 index 0000000..bd57650 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/cluster/DynamicAddConsumerIT.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.test.client.consumer.cluster; + +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.consumer.balance.NormalMsgStaticBalanceIT; +import org.apache.rocketmq.test.client.mq.MQAsyncProducer; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.MQWait; +import org.apache.rocketmq.test.util.TestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class DynamicAddConsumerIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(NormalMsgStaticBalanceIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("use topic: %s !", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testAddOneConsumer() { + int msgSize = 100; + RMQNormalConsumer consumer1 = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + + MQAsyncProducer asyncDefaultMQProducer = new MQAsyncProducer(producer, msgSize, 100); + asyncDefaultMQProducer.start(); + TestUtils.waitForSeconds(WAIT_TIME); + + RMQNormalConsumer consumer2 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), topic, + "*", new RMQNormalListener()); + + asyncDefaultMQProducer.waitSendAll(WAIT_TIME * 6); + + MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), consumer1.getListener(), + consumer2.getListener()); + + boolean recvAll = MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), + consumer1.getListener(), consumer2.getListener()); + assertThat(recvAll).isEqualTo(true); + } + + @Test + public void testAddTwoConsumer() { + int msgSize = 100; + RMQNormalConsumer consumer1 = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + + MQAsyncProducer asyncDefaultMQProducer = new MQAsyncProducer(producer, msgSize, 100); + asyncDefaultMQProducer.start(); + TestUtils.waitForSeconds(WAIT_TIME); + + RMQNormalConsumer consumer2 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), topic, + "*", new RMQNormalListener()); + RMQNormalConsumer consumer3 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), topic, + "*", new RMQNormalListener()); + + asyncDefaultMQProducer.waitSendAll(WAIT_TIME * 6); + + MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), consumer1.getListener(), + consumer2.getListener(), consumer3.getListener()); + + boolean recvAll = MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), + consumer1.getListener(), consumer2.getListener(), consumer3.getListener()); + assertThat(recvAll).isEqualTo(true); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/cluster/DynamicCrashConsumerIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/cluster/DynamicCrashConsumerIT.java new file mode 100644 index 0000000..003bd64 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/cluster/DynamicCrashConsumerIT.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.test.client.consumer.cluster; + +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.consumer.balance.NormalMsgStaticBalanceIT; +import org.apache.rocketmq.test.client.mq.MQAsyncProducer; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.MQWait; +import org.apache.rocketmq.test.util.TestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class DynamicCrashConsumerIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(NormalMsgStaticBalanceIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("use topic: %s !", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testAddOneConsumer() { + int msgSize = 100; + RMQNormalConsumer consumer1 = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + RMQNormalConsumer consumer2 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), topic, + "*", new RMQNormalListener()); + + MQAsyncProducer asyncDefaultMQProducer = new MQAsyncProducer(producer, msgSize, 100); + asyncDefaultMQProducer.start(); + TestUtils.waitForSeconds(WAIT_TIME); + + consumer2.shutdown(); + + asyncDefaultMQProducer.waitSendAll(WAIT_TIME * 6); + + MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), consumer1.getListener(), + consumer2.getListener()); + + boolean recvAll = MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), + consumer1.getListener(), consumer2.getListener()); + assertThat(recvAll).isEqualTo(true); + } + + @Test + public void testAddTwoConsumer() { + int msgSize = 100; + RMQNormalConsumer consumer1 = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + RMQNormalConsumer consumer2 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), topic, + "*", new RMQNormalListener()); + RMQNormalConsumer consumer3 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), topic, + "*", new RMQNormalListener()); + + MQAsyncProducer asyncDefaultMQProducer = new MQAsyncProducer(producer, msgSize, 100); + asyncDefaultMQProducer.start(); + TestUtils.waitForSeconds(WAIT_TIME); + + consumer2.shutdown(); + consumer3.shutdown(); + + asyncDefaultMQProducer.waitSendAll(WAIT_TIME * 6); + + MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), consumer1.getListener(), + consumer2.getListener(), consumer3.getListener()); + + boolean recvAll = MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), + consumer1.getListener(), consumer2.getListener(), consumer3.getListener()); + assertThat(recvAll).isEqualTo(true); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/filter/SqlFilterIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/filter/SqlFilterIT.java new file mode 100644 index 0000000..88afbee --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/filter/SqlFilterIT.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.test.client.consumer.filter; + +import java.util.ArrayList; +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.MessageSelector; +import org.apache.rocketmq.client.consumer.PullResult; +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.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.client.rmq.RMQSqlConsumer; +import org.apache.rocketmq.test.factory.ConsumerFactory; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class SqlFilterIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(SqlFilterIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + private static final Map OFFSE_TABLE = new HashMap(); + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("use topic: %s;", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + OFFSE_TABLE.clear(); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testFilterConsumer() throws Exception { + int msgSize = 16; + + String group = initConsumerGroup(); + MessageSelector selector = MessageSelector.bySql("(TAGS is not null and TAGS in ('TagA', 'TagB'))"); + RMQSqlConsumer consumer = ConsumerFactory.getRMQSqlConsumer(NAMESRV_ADDR, group, topic, selector, new RMQNormalListener(group + "_1")); + Thread.sleep(3000); + producer.send("TagA", msgSize); + producer.send("TagB", msgSize); + producer.send("TagC", msgSize); + Assert.assertEquals("Not all sent succeeded", msgSize * 3, producer.getAllUndupMsgBody().size()); + consumer.getListener().waitForMessageConsume(msgSize * 2, CONSUME_TIME); + assertThat(producer.getAllMsgBody()) + .containsAllIn(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())); + + assertThat(consumer.getListener().getAllMsgBody().size()).isEqualTo(msgSize * 2); + } + + @Test + public void testFilterPullConsumer() throws Exception { + int msgSize = 16; + + String group = initConsumerGroup(); + MessageSelector selector = MessageSelector.bySql("(TAGS is not null and TAGS in ('TagA', 'TagB'))"); + DefaultMQPullConsumer consumer = new DefaultMQPullConsumer(group); + consumer.setNamesrvAddr(NAMESRV_ADDR); + consumer.start(); + Thread.sleep(3000); + producer.send("TagA", msgSize); + producer.send("TagB", msgSize); + producer.send("TagC", msgSize); + Assert.assertEquals("Not all sent succeeded", msgSize * 3, producer.getAllUndupMsgBody().size()); + + List receivedMessage = new ArrayList<>(2); + Set mqs = consumer.fetchSubscribeMessageQueues(topic); + for (MessageQueue mq : mqs) { + SINGLE_MQ: + while (true) { + try { + PullResult pullResult = + consumer.pull(mq, selector, getMessageQueueOffset(mq), 32); + putMessageQueueOffset(mq, pullResult.getNextBeginOffset()); + switch (pullResult.getPullStatus()) { + case FOUND: + List msgs = pullResult.getMsgFoundList(); + for (MessageExt msg : msgs) { + receivedMessage.add(new String(msg.getBody())); + } + break; + case NO_MATCHED_MSG: + break; + case NO_NEW_MSG: + break SINGLE_MQ; + case OFFSET_ILLEGAL: + break; + default: + break; + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + assertThat(receivedMessage.size()).isEqualTo(msgSize * 2); + } + + 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); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/BasePop.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/BasePop.java new file mode 100644 index 0000000..29ff902 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/BasePop.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.test.client.consumer.pop; + +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQPopClient; +import org.apache.rocketmq.test.factory.ConsumerFactory; + +public class BasePop extends BaseConf { + + public RMQPopClient getRMQPopClient() { + RMQPopClient client = ConsumerFactory.getRMQPopClient(); + mqClients.add(client); + return client; + } + + protected static class MsgRcv { + public final long rcvTime; + public final MessageExt messageExt; + + public MsgRcv(long rcvTime, MessageExt messageExt) { + this.rcvTime = rcvTime; + this.messageExt = messageExt; + } + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/BasePopNormally.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/BasePopNormally.java new file mode 100644 index 0000000..2e29b95 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/BasePopNormally.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.test.client.consumer.pop; + +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.client.consumer.PopResult; +import org.apache.rocketmq.common.attribute.CQType; +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.common.constant.ConsumeInitMode; +import org.apache.rocketmq.common.filter.ExpressionType; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.test.base.IntegrationTestBase; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.client.rmq.RMQPopClient; +import org.apache.rocketmq.test.util.MQRandomUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; + +@Ignore +public class BasePopNormally extends BasePop { + + protected String topic; + protected String group; + protected RMQNormalProducer producer = null; + protected RMQPopClient client = null; + protected String brokerAddr; + protected MessageQueue messageQueue; + + @Before + public void setUp() { + brokerAddr = brokerController1.getBrokerAddr(); + topic = MQRandomUtils.getRandomTopic(); + group = initConsumerGroup(); + IntegrationTestBase.initTopic(topic, NAMESRV_ADDR, BROKER1_NAME, 8, CQType.SimpleCQ, TopicMessageType.NORMAL); + producer = getProducer(NAMESRV_ADDR, topic); + client = getRMQPopClient(); + messageQueue = new MessageQueue(topic, BROKER1_NAME, -1); + } + + @After + public void tearDown() { + shutdown(); + } + + protected CompletableFuture popMessageAsync(long invisibleTime, int maxNums, long timeout) { + return client.popMessageAsync( + brokerAddr, messageQueue, invisibleTime, maxNums, group, timeout, true, + ConsumeInitMode.MIN, false, ExpressionType.TAG, "*"); + } + + protected CompletableFuture popMessageAsync(long invisibleTime, int maxNums) { + return client.popMessageAsync( + brokerAddr, messageQueue, invisibleTime, maxNums, group, 3000, false, + ConsumeInitMode.MIN, false, ExpressionType.TAG, "*"); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/BasePopOrderly.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/BasePopOrderly.java new file mode 100644 index 0000000..acf70f7 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/BasePopOrderly.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.test.client.consumer.pop; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.client.consumer.PopResult; +import org.apache.rocketmq.common.attribute.CQType; +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.common.constant.ConsumeInitMode; +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.message.MessageQueue; +import org.apache.rocketmq.test.base.IntegrationTestBase; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.client.rmq.RMQPopClient; +import org.apache.rocketmq.test.message.MessageQueueMsg; +import org.apache.rocketmq.test.util.MQRandomUtils; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.assertj.core.util.Lists; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; + +import static org.junit.Assert.assertEquals; + +@Ignore +public class BasePopOrderly extends BasePop { + protected String topic; + protected String group; + protected RMQNormalProducer producer = null; + protected RMQPopClient client = null; + protected String brokerAddr; + protected MessageQueue messageQueue; + protected final Map> msgRecv = new ConcurrentHashMap<>(); + protected final List msgRecvSequence = new CopyOnWriteArrayList<>(); + protected final List msgDataRecv = new CopyOnWriteArrayList<>(); + + @Before + public void setUp() { + brokerController1.getBrokerConfig().setEnableNotifyAfterPopOrderLockRelease(true); + brokerAddr = brokerController1.getBrokerAddr(); + topic = MQRandomUtils.getRandomTopic(); + group = initConsumerGroup(); + IntegrationTestBase.initTopic(topic, NAMESRV_ADDR, BROKER1_NAME, 1, CQType.SimpleCQ, TopicMessageType.FIFO); + producer = getProducer(NAMESRV_ADDR, topic); + client = getRMQPopClient(); + messageQueue = new MessageQueue(topic, BROKER1_NAME, -1); + } + + @After + public void tearDown() { + shutdown(); + } + + protected void sendMessage(int num) { + MessageQueueMsg mqMsgs = new MessageQueueMsg(Lists.newArrayList(messageQueue), num); + producer.send(mqMsgs.getMsgsWithMQ()); + } + + protected void assertMessageRecvOrder() { + VerifyUtils.verifyOrderMsg(msgDataRecv); + } + + protected void assertMsgRecv(int seqId, int expectNum) { + String msgId = msgRecvSequence.get(seqId); + List msgRcvList = msgRecv.get(msgId); + assertEquals(expectNum, msgRcvList.size()); + assertConsumeTimes(msgRcvList); + } + + protected void assertConsumeTimes(List msgRcvList) { + for (int i = 0; i < msgRcvList.size(); i++) { + assertEquals(i, msgRcvList.get(i).messageExt.getReconsumeTimes()); + } + } + + protected void assertMsgRecv(int seqId, int expectNum, List expectReconsumeTimes) { + String msgId = msgRecvSequence.get(seqId); + List msgRcvList = msgRecv.get(msgId); + assertEquals(expectNum, msgRcvList.size()); + assertConsumeTimes(msgRcvList, expectReconsumeTimes); + } + + protected void assertConsumeTimes(List msgRcvList, List expectReconsumeTimes) { + for (int i = 0; i < msgRcvList.size(); i++) { + assertEquals(expectReconsumeTimes.get(i).intValue(), msgRcvList.get(i).messageExt.getReconsumeTimes()); + } + } + + protected void onRecvNewMessage(MessageExt messageExt) { + msgDataRecv.add(new String(messageExt.getBody())); + msgRecvSequence.add(messageExt.getMsgId()); + msgRecv.compute(messageExt.getMsgId(), (k, msgRcvList) -> { + if (msgRcvList == null) { + msgRcvList = new CopyOnWriteArrayList<>(); + } + msgRcvList.add(new MsgRcv(System.currentTimeMillis(), messageExt)); + return msgRcvList; + }); + } + + protected CompletableFuture popMessageOrderlyAsync(long invisibleTime, int maxNums, long timeout) { + return popMessageOrderlyAsync(invisibleTime, maxNums, timeout, null); + } + + protected CompletableFuture popMessageOrderlyAsync(long invisibleTime, int maxNums, long timeout, String attemptId) { + return client.popMessageAsync( + brokerAddr, messageQueue, invisibleTime, maxNums, group, timeout, true, + ConsumeInitMode.MIN, true, ExpressionType.TAG, "*", attemptId); + } + + protected CompletableFuture ackMessageAsync(MessageExt messageExt) { + return client.ackMessageAsync(brokerAddr, topic, group, messageExt.getProperty(MessageConst.PROPERTY_POP_CK)); + } + + protected CompletableFuture changeInvisibleTimeAsync(MessageExt messageExt, long invisibleTime) { + return client.changeInvisibleTimeAsync( + brokerAddr, BROKER1_NAME, topic, group, + messageExt.getProperty(MessageConst.PROPERTY_POP_CK), invisibleTime); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/BatchAckIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/BatchAckIT.java new file mode 100644 index 0000000..ec9153c --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/BatchAckIT.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. + */ + +package org.apache.rocketmq.test.client.consumer.pop; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; +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.common.attribute.CQType; +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.common.constant.ConsumeInitMode; +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.message.MessageQueue; +import org.apache.rocketmq.test.base.IntegrationTestBase; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.client.rmq.RMQPopClient; +import org.apache.rocketmq.test.util.MQRandomUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; + +public class BatchAckIT extends BasePop { + + protected String topic; + protected String group; + protected RMQNormalProducer producer = null; + protected RMQPopClient client = null; + protected String brokerAddr; + protected MessageQueue messageQueue; + + @Before + public void setUp() { + brokerAddr = brokerController1.getBrokerAddr(); + topic = MQRandomUtils.getRandomTopic(); + group = initConsumerGroup(); + IntegrationTestBase.initTopic(topic, NAMESRV_ADDR, BROKER1_NAME, 8, CQType.SimpleCQ, TopicMessageType.NORMAL); + producer = getProducer(NAMESRV_ADDR, topic); + client = getRMQPopClient(); + messageQueue = new MessageQueue(topic, BROKER1_NAME, -1); + } + + @After + public void tearDown() { + shutdown(); + } + + @Test + public void testBatchAckNormallyWithPopBuffer() throws Throwable { + brokerController1.getBrokerConfig().setEnablePopBufferMerge(true); + brokerController2.getBrokerConfig().setEnablePopBufferMerge(true); + + testBatchAck(() -> { + try { + return popMessageAsync().get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testBatchAckNormallyWithOutPopBuffer() throws Throwable { + brokerController1.getBrokerConfig().setEnablePopBufferMerge(false); + brokerController2.getBrokerConfig().setEnablePopBufferMerge(false); + + testBatchAck(() -> { + try { + return popMessageAsync().get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testBatchAckOrderly() throws Throwable { + testBatchAck(() -> { + try { + return popMessageOrderlyAsync().get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + public void testBatchAck(Supplier popResultSupplier) throws Throwable { + // Send 10 messages but do not ack, let them enter the retry topic + producer.send(10); + AtomicInteger firstMsgRcvNum = new AtomicInteger(); + await().atMost(Duration.ofSeconds(3)).untilAsserted(() -> { + PopResult popResult = popResultSupplier.get(); + if (popResult.getPopStatus().equals(PopStatus.FOUND)) { + firstMsgRcvNum.addAndGet(popResult.getMsgFoundList().size()); + } + assertEquals(10, firstMsgRcvNum.get()); + }); + // sleep 6s, expect messages to enter the retry topic + TimeUnit.SECONDS.sleep(6); + + producer.send(20); + List extraInfoList = new ArrayList<>(); + await().atMost(Duration.ofSeconds(3)).untilAsserted(() -> { + PopResult popResult = popResultSupplier.get(); + if (popResult.getPopStatus().equals(PopStatus.FOUND)) { + for (MessageExt messageExt : popResult.getMsgFoundList()) { + extraInfoList.add(messageExt.getProperty(MessageConst.PROPERTY_POP_CK)); + } + } + assertEquals(30, extraInfoList.size()); + }); + + AckResult ackResult = client.batchAckMessageAsync(brokerAddr, topic, group, extraInfoList).get(); + assertEquals(AckStatus.OK, ackResult.getStatus()); + + // sleep 6s, expected that messages that have been acked will not be re-consumed + TimeUnit.SECONDS.sleep(6); + PopResult popResult = popResultSupplier.get(); + assertEquals(PopStatus.POLLING_NOT_FOUND, popResult.getPopStatus()); + } + + private CompletableFuture popMessageAsync() { + return client.popMessageAsync( + brokerAddr, messageQueue, Duration.ofSeconds(3).toMillis(), 30, group, 3000, false, + ConsumeInitMode.MIN, false, ExpressionType.TAG, "*"); + } + + private CompletableFuture popMessageOrderlyAsync() { + return client.popMessageAsync( + brokerAddr, messageQueue, Duration.ofSeconds(3).toMillis(), 30, group, 3000, false, + ConsumeInitMode.MIN, true, ExpressionType.TAG, "*", null); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/NotificationIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/NotificationIT.java new file mode 100644 index 0000000..3a6ad06 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/NotificationIT.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.test.client.consumer.pop; + +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.common.attribute.CQType; +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.common.constant.ConsumeInitMode; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.test.base.IntegrationTestBase; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.client.rmq.RMQPopClient; +import org.apache.rocketmq.test.message.MessageQueueMsg; +import org.apache.rocketmq.test.util.MQRandomUtils; +import org.assertj.core.util.Lists; +import org.junit.Before; +import org.junit.Test; +import org.junit.Ignore; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class NotificationIT extends BasePop { + protected String topic; + protected String group; + protected RMQNormalProducer producer = null; + protected RMQPopClient client = null; + protected String brokerAddr; + protected MessageQueue messageQueue; + + @Before + public void setUp() { + brokerAddr = brokerController1.getBrokerAddr(); + topic = MQRandomUtils.getRandomTopic(); + group = initConsumerGroup(); + IntegrationTestBase.initTopic(topic, NAMESRV_ADDR, BROKER1_NAME, 8, CQType.SimpleCQ, TopicMessageType.NORMAL); + producer = getProducer(NAMESRV_ADDR, topic); + client = getRMQPopClient(); + messageQueue = new MessageQueue(topic, BROKER1_NAME, -1); + } + + @Test + @Ignore + public void testNotification() throws Exception { + long pollTime = 500; + CompletableFuture future1 = client.notification(brokerAddr, topic, group, messageQueue.getQueueId(), pollTime, System.currentTimeMillis(), 5000); + CompletableFuture future2 = client.notification(brokerAddr, topic, group, messageQueue.getQueueId(), pollTime, System.currentTimeMillis(), 5000); + sendMessage(1); + Boolean result2 = future2.get(); + assertThat(result2).isTrue(); + client.popMessageAsync(brokerAddr, messageQueue, 10000, 1, group, 1000, false, + ConsumeInitMode.MIN, false, null, null).get(); + Boolean result1 = future1.get(); + assertThat(result1).isFalse(); + } + + @Test + public void testNotificationOrderly() throws Exception { + long pollTime = 500; + String attemptId = "attemptId"; + CompletableFuture future1 = client.notification(brokerAddr, topic, group, messageQueue.getQueueId(), true, attemptId, pollTime, System.currentTimeMillis(), 5000); + CompletableFuture future2 = client.notification(brokerAddr, topic, group, messageQueue.getQueueId(), true, attemptId, pollTime, System.currentTimeMillis(), 5000); + sendMessage(1); + Boolean result1 = future1.get(); + assertThat(result1).isTrue(); + client.popMessageAsync(brokerAddr, messageQueue, 10000, 1, group, 1000, false, + ConsumeInitMode.MIN, true, null, null, attemptId).get(); + Boolean result2 = future2.get(); + assertThat(result2).isTrue(); + + String attemptId2 = "attemptId2"; + CompletableFuture future3 = client.notification(brokerAddr, topic, group, messageQueue.getQueueId(), true, attemptId2, pollTime, System.currentTimeMillis(), 5000); + assertThat(future3.get()).isFalse(); + } + + protected void sendMessage(int num) { + MessageQueueMsg mqMsgs = new MessageQueueMsg(Lists.newArrayList(messageQueue), num); + producer.send(mqMsgs.getMsgsWithMQ()); + } + +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/PopBigMessageIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/PopBigMessageIT.java new file mode 100644 index 0000000..c8d6da2 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/PopBigMessageIT.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.test.client.consumer.pop; + +import java.io.IOException; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.consumer.PopResult; +import org.apache.rocketmq.client.consumer.PopStatus; +import org.apache.rocketmq.common.compression.Compressor; +import org.apache.rocketmq.common.compression.CompressorFactory; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.junit.Test; + +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; + +public class PopBigMessageIT extends BasePopNormally { + + private static final int BODY_LEN = 3 * 1024 * 1024; + + static { + System.setProperty(ClientConfig.DECODE_DECOMPRESS_BODY, "false"); + } + + private Message createBigMessage() { + byte[] bytes = new byte[BODY_LEN]; + return new Message(topic, bytes); + } + + @Test + public void testSendAndRecvBigMsgWhenDisablePopBufferMerge() throws Throwable { + brokerController1.getBrokerConfig().setEnablePopBufferMerge(false); + brokerController2.getBrokerConfig().setEnablePopBufferMerge(false); + + this.testSendAndRecvBigMsg(); + } + + @Test + public void testSendAndRecvBigMsgWhenEnablePopBufferMerge() throws Throwable { + brokerController1.getBrokerConfig().setEnablePopBufferMerge(true); + brokerController2.getBrokerConfig().setEnablePopBufferMerge(true); + + this.testSendAndRecvBigMsg(); + } + + /** + * set DECODE_DECOMPRESS_BODY to false, then pop message from broker and not ack + *

    + * expect when re-consume this message, the message is not decompressed + */ + private void testSendAndRecvBigMsg() { + Message message = createBigMessage(); + producer.send(message); + + AtomicReference firstMessageExtRef = new AtomicReference<>(); + await().atMost(Duration.ofSeconds(3)).untilAsserted(() -> { + PopResult popResult = popMessageAsync(Duration.ofSeconds(3).toMillis(), 1, 5000).get(); + assertEquals(PopStatus.FOUND, popResult.getPopStatus()); + + firstMessageExtRef.set(popResult.getMsgFoundList().get(0)); + MessageExt messageExt = firstMessageExtRef.get(); + assertMessageRecv(messageExt); + }); + + // no ack, msg will put into pop retry topic + await().atMost(Duration.ofSeconds(60)).untilAsserted(() -> { + PopResult retryPopResult = popMessageAsync(Duration.ofSeconds(3).toMillis(), 1, 5000).get(); + assertEquals(PopStatus.FOUND, retryPopResult.getPopStatus()); + + MessageExt retryMessageExt = retryPopResult.getMsgFoundList().get(0); + assertMessageRecv(retryMessageExt); + assertEquals(firstMessageExtRef.get().getBody().length, retryMessageExt.getBody().length); + }); + } + + private void assertMessageRecv(MessageExt messageExt) throws IOException { + assertEquals(MessageSysFlag.COMPRESSED_FLAG, messageExt.getSysFlag() & MessageSysFlag.COMPRESSED_FLAG); + Compressor compressor = CompressorFactory.getCompressor(MessageSysFlag.getCompressionType(messageExt.getSysFlag())); + assertEquals(BODY_LEN, compressor.decompress(messageExt.getBody()).length); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/PopMessageAndForwardingIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/PopMessageAndForwardingIT.java new file mode 100644 index 0000000..52a0c27 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/PopMessageAndForwardingIT.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.test.client.consumer.pop; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.rocketmq.client.consumer.PopResult; +import org.apache.rocketmq.client.consumer.PopStatus; +import org.apache.rocketmq.common.attribute.CQType; +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.common.constant.ConsumeInitMode; +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.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.header.ExtraInfoUtil; +import org.apache.rocketmq.test.base.IntegrationTestBase; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.client.rmq.RMQPopClient; +import org.apache.rocketmq.test.util.MQRandomUtils; +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.assertNotEquals; + +public class PopMessageAndForwardingIT extends BasePop { + + protected String topic; + protected String group; + protected RMQNormalProducer producer = null; + protected RMQPopClient client = null; + protected String broker1Addr; + protected MessageQueue broker1MessageQueue; + protected String broker2Addr; + protected MessageQueue broker2MessageQueue; + + @Before + public void setUp() { + broker1Addr = brokerController1.getBrokerAddr(); + broker2Addr = brokerController2.getBrokerAddr(); + topic = MQRandomUtils.getRandomTopic(); + group = initConsumerGroup(); + IntegrationTestBase.initTopic(topic, NAMESRV_ADDR, BROKER1_NAME, 8, CQType.SimpleCQ, TopicMessageType.NORMAL); + IntegrationTestBase.initTopic(topic, NAMESRV_ADDR, BROKER2_NAME, 8, CQType.SimpleCQ, TopicMessageType.NORMAL); + producer = getProducer(NAMESRV_ADDR, topic); + client = getRMQPopClient(); + broker1MessageQueue = new MessageQueue(topic, BROKER1_NAME, -1); + broker2MessageQueue = new MessageQueue(topic, BROKER2_NAME, -1); + } + + @Test + public void test() { + producer.send(1, broker1MessageQueue); + + AtomicReference firstMessageExtRef = new AtomicReference<>(); + await().atMost(Duration.ofSeconds(3)).until(() -> { + PopResult popResult = client.popMessageAsync(broker1Addr, broker1MessageQueue, 3000, 32, group, 1000, + true, ConsumeInitMode.MIN, false, ExpressionType.TAG, "*").get(); + if (!popResult.getPopStatus().equals(PopStatus.FOUND)) { + return false; + } + firstMessageExtRef.set(popResult.getMsgFoundList().get(0)); + return true; + }); + + producer.sendMQ(firstMessageExtRef.get(), broker2MessageQueue); + AtomicReference secondMessageExtRef = new AtomicReference<>(); + await().atMost(Duration.ofSeconds(3)).until(() -> { + PopResult popResult = client.popMessageAsync(broker2Addr, broker2MessageQueue, 3000, 32, group, 1000, + true, ConsumeInitMode.MIN, false, ExpressionType.TAG, "*").get(); + if (!popResult.getPopStatus().equals(PopStatus.FOUND)) { + return false; + } + secondMessageExtRef.set(popResult.getMsgFoundList().get(0)); + return true; + }); + + assertEquals(firstMessageExtRef.get().getMsgId(), secondMessageExtRef.get().getMsgId()); + String firstPopCk = firstMessageExtRef.get().getProperty(MessageConst.PROPERTY_POP_CK); + String secondPopCk = secondMessageExtRef.get().getProperty(MessageConst.PROPERTY_POP_CK); + assertNotEquals(firstPopCk, secondPopCk); + assertEquals(BROKER1_NAME, ExtraInfoUtil.getBrokerName(ExtraInfoUtil.split(firstPopCk))); + assertEquals(BROKER2_NAME, ExtraInfoUtil.getBrokerName(ExtraInfoUtil.split(secondPopCk))); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/PopOrderlyIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/PopOrderlyIT.java new file mode 100644 index 0000000..efb12a3 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/PopOrderlyIT.java @@ -0,0 +1,289 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.test.client.consumer.pop; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.client.consumer.PopResult; +import org.apache.rocketmq.common.message.MessageExt; +import org.assertj.core.util.Lists; +import org.junit.Test; + +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; + +public class PopOrderlyIT extends BasePopOrderly { + + /** + * send 10 messages, pop one message orderly at a time + *

    + * expect receive this 10 messages in order + */ + @Test + public void testPopOrderly() { + sendMessage(10); + await().atMost(Duration.ofSeconds(10)).until(() -> { + popMessageOrderly().get(); + return msgRecv.size() == 10; + }); + + assertMessageRecvOrder(); + } + + private CompletableFuture popMessageOrderly() { + CompletableFuture future = popMessageOrderlyAsync(TimeUnit.SECONDS.toMillis(3), 1, TimeUnit.SECONDS.toMillis(30)); + CompletableFuture resultFuture = new CompletableFuture<>(); + future.whenComplete((popResult, throwable) -> { + if (throwable != null) { + resultFuture.completeExceptionally(throwable); + return; + } + if (popResult.getMsgFoundList() == null || popResult.getMsgFoundList().isEmpty()) { + resultFuture.complete(null); + return; + } + try { + for (MessageExt messageExt : popResult.getMsgFoundList()) { + onRecvNewMessage(messageExt); + // ack later + // expect when the lock is free, pop message request can receive messages immediately after ack + new Thread(() -> { + try { + TimeUnit.MILLISECONDS.sleep(100); + } catch (InterruptedException ignored) { + } + ackMessageAsync(messageExt); + }).start(); + } + resultFuture.complete(null); + } catch (Throwable t) { + resultFuture.completeExceptionally(t); + } + }); + return resultFuture; + } + + /** + * send 10 messages, pop five messages orderly at a time + *

    + * expect only receive the first five messages + */ + @Test + public void testPopOrderlyThenNoAck() { + sendMessage(10); + await().atMost(Duration.ofSeconds(5)).until(() -> { + popOrderlyThenNoAck().get(); + return msgRecvSequence.size() == 10; + }); + assertEquals(5, msgRecv.size()); + for (Map.Entry> entry : msgRecv.entrySet()) { + assertEquals(2, entry.getValue().size()); + for (int i = 0; i < entry.getValue().size(); i++) { + assertEquals(i, entry.getValue().get(i).messageExt.getReconsumeTimes()); + } + } + assertMessageRecvOrder(); + } + + private CompletableFuture popOrderlyThenNoAck() { + CompletableFuture future = popMessageOrderlyAsync(TimeUnit.SECONDS.toMillis(3), 5, TimeUnit.SECONDS.toMillis(30)); + CompletableFuture resultFuture = new CompletableFuture<>(); + future.whenComplete((popResult, throwable) -> { + if (throwable != null) { + resultFuture.completeExceptionally(throwable); + return; + } + if (popResult.getMsgFoundList() == null || popResult.getMsgFoundList().isEmpty()) { + resultFuture.complete(null); + return; + } + try { + for (MessageExt messageExt : popResult.getMsgFoundList()) { + onRecvNewMessage(messageExt); + } + resultFuture.complete(null); + } catch (Throwable t) { + resultFuture.completeExceptionally(t); + } + }); + return resultFuture; + } + + /** + * send one message, changeInvisibleTime to 5s later at the first time + *

    + * expect receive two times + */ + @Test + public void testPopMessageOrderlyThenChangeInvisibleTime() { + sendMessage(1); + + await().atMost(Duration.ofSeconds(15)).until(() -> { + popMessageOrderlyThenChangeInvisibleTime().get(); + return msgRecvSequence.size() == 2; + }); + + assertMsgRecv(1, 2); + assertMessageRecvOrder(); + } + + private CompletableFuture popMessageOrderlyThenChangeInvisibleTime() { + CompletableFuture future = popMessageOrderlyAsync(TimeUnit.SECONDS.toMillis(3), 1, TimeUnit.SECONDS.toMillis(30)); + CompletableFuture resultFuture = new CompletableFuture<>(); + future.whenComplete((popResult, throwable) -> { + if (throwable != null) { + resultFuture.completeExceptionally(throwable); + return; + } + if (popResult.getMsgFoundList() == null || popResult.getMsgFoundList().isEmpty()) { + resultFuture.complete(null); + return; + } + try { + for (MessageExt messageExt : popResult.getMsgFoundList()) { + onRecvNewMessage(messageExt); + if (msgRecvSequence.size() == 1) { + try { + TimeUnit.MILLISECONDS.sleep(1); + changeInvisibleTimeAsync(messageExt, 5000).get(); + } catch (Exception e) { + resultFuture.completeExceptionally(e); + return; + } + } else { + try { + ackMessageAsync(messageExt).get(); + } catch (Exception e) { + resultFuture.completeExceptionally(e); + return; + } + } + } + resultFuture.complete(null); + } catch (Throwable t) { + resultFuture.completeExceptionally(t); + } + }); + return resultFuture; + } + + /** + * send three messages (msg1, msg2, msg3, msg4) and the max message num of pop request is three + *

    + * ack msg1 and msg3, changeInvisibleTime msg2 + *

    + * expect the sequence of messages received is: msg1, msg2, msg3, msg2, msg3, msg4 + */ + @Test + public void testPopMessageOrderlyThenChangeInvisibleTimeMidMessage() { + producer.send(4); + + await().atMost(Duration.ofSeconds(5)).until(() -> { + popMessageOrderlyThenChangeInvisibleTimeMidMessage().get(); + return msgRecvSequence.size() == 6; + }); + + assertMsgRecv(0, 1); + assertMsgRecv(1, 2); + assertMsgRecv(2, 2); + assertMsgRecv(5, 1); + + assertEquals(msgRecvSequence.get(1), msgRecvSequence.get(3)); + assertEquals(msgRecvSequence.get(2), msgRecvSequence.get(4)); + } + + private CompletableFuture popMessageOrderlyThenChangeInvisibleTimeMidMessage() { + CompletableFuture future = popMessageOrderlyAsync(5000, 3, TimeUnit.SECONDS.toMillis(30)); + CompletableFuture resultFuture = new CompletableFuture<>(); + future.whenComplete((popResult, throwable) -> { + if (throwable != null) { + resultFuture.completeExceptionally(throwable); + return; + } + if (popResult.getMsgFoundList() == null || popResult.getMsgFoundList().isEmpty()) { + resultFuture.complete(null); + return; + } + try { + for (MessageExt messageExt : popResult.getMsgFoundList()) { + onRecvNewMessage(messageExt); + if (msgRecv.size() != 2) { + try { + ackMessageAsync(messageExt).get(); + } catch (Exception e) { + resultFuture.completeExceptionally(e); + return; + } + } else { + try { + TimeUnit.MILLISECONDS.sleep(1); + changeInvisibleTimeAsync(messageExt, 3000).get(); + } catch (Exception e) { + resultFuture.completeExceptionally(e); + return; + } + } + } + resultFuture.complete(null); + } catch (Throwable t) { + resultFuture.completeExceptionally(t); + } + }); + return resultFuture; + } + + @Test + public void testReentrant() { + producer.send(1); + + popMessageForReentrant(null).join(); + assertMsgRecv(0, 1, Lists.newArrayList(0)); + + String attemptId01 = "attemptId-01"; + popMessageForReentrant(attemptId01).join(); + assertMsgRecv(0, 2, Lists.newArrayList(0, 1)); + popMessageForReentrant(attemptId01).join(); + assertMsgRecv(0, 3, Lists.newArrayList(0, 1, 1)); + + String attemptId02 = "attemptId-02"; + await().atLeast(Duration.ofSeconds(5)).atMost(Duration.ofSeconds(15)).until(() -> { + popMessageForReentrant(attemptId02).join(); + return msgRecvSequence.size() == 4; + }); + popMessageForReentrant(attemptId02).join(); + assertMsgRecv(0, 5, Lists.newArrayList(0, 1, 1, 2, 2)); + + await().atLeast(Duration.ofSeconds(5)).atMost(Duration.ofSeconds(15)).until(() -> { + popMessageForReentrant(null).join(); + return msgRecvSequence.size() == 6; + }); + assertMsgRecv(0, 6, Lists.newArrayList(0, 1, 1, 2, 2, 3)); + } + + private CompletableFuture popMessageForReentrant(String attemptId) { + return popMessageOrderlyAsync(TimeUnit.SECONDS.toMillis(10), 3, TimeUnit.SECONDS.toMillis(30), attemptId) + .thenAccept(popResult -> { + for (MessageExt messageExt : popResult.getMsgFoundList()) { + onRecvNewMessage(messageExt); + } + }); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/PopSubCheckIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/PopSubCheckIT.java new file mode 100644 index 0000000..3034876 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/PopSubCheckIT.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.test.client.consumer.pop; + +import org.apache.rocketmq.common.message.MessageClientExt; +import org.apache.rocketmq.common.message.MessageConst; +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.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.client.rmq.RMQPopConsumer; +import org.apache.rocketmq.test.factory.ConsumerFactory; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.RandomUtil; +import org.apache.rocketmq.test.util.TestUtils; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class PopSubCheckIT extends BaseConf { + private static final Logger log = LoggerFactory.getLogger(PopSubCheckIT.class); + private String group; + + private DefaultMQAdminExt defaultMQAdminExt; + + @Before + public void setUp() throws Exception { + group = initConsumerGroup(); + + defaultMQAdminExt = new DefaultMQAdminExt(); + defaultMQAdminExt.setInstanceName(RandomUtil.getStringByUUID()); + defaultMQAdminExt.start(); + } + + @After + public void tearDown() { + defaultMQAdminExt.shutdown(); + super.shutdown(); + } + + @Ignore + @Test + public void testNormalPopAck() throws Exception { + String topic = initTopic(); + log.info("use topic: {}; group: {} !", topic, group); + + RMQNormalProducer producer = getProducer(NAMESRV_ADDR, topic); + producer.getProducer().setCompressMsgBodyOverHowmuch(Integer.MAX_VALUE); + + for (String brokerAddr : new String[]{brokerController1.getBrokerAddr(), brokerController2.getBrokerAddr()}) { + defaultMQAdminExt.setMessageRequestMode(brokerAddr, topic, group, MessageRequestMode.POP, 8, 60_000); + } + + RMQPopConsumer consumer = ConsumerFactory.getRMQPopConsumer(NAMESRV_ADDR, group, + topic, "*", new RMQNormalListener()); + mqClients.add(consumer); + + int msgNum = 1; + producer.send(msgNum); + Assert.assertEquals("Not all sent succeeded", msgNum, producer.getAllUndupMsgBody().size()); + log.info(producer.getFirstMsg().toString()); + + TestUtils.waitForSeconds(10); + + consumer.getListener().waitForMessageConsume(msgNum, 30_000); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + for (Object o : consumer.getListener().getAllOriginMsg()) { + MessageClientExt msg = (MessageClientExt) o; + assertThat(msg.getProperty(MessageConst.PROPERTY_POP_CK)).named("check pop meta").isNotEmpty(); + } + + consumer.getListener().waitForMessageConsume(msgNum, 3_000 * 9); + assertThat(consumer.getListener().getAllOriginMsg().size()).isEqualTo(msgNum); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/tag/MulTagSubIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/tag/MulTagSubIT.java new file mode 100644 index 0000000..e5df98b --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/tag/MulTagSubIT.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.test.client.consumer.tag; + +import java.util.List; + +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.factory.MQMessageFactory; +import org.apache.rocketmq.test.factory.TagMessage; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.TestUtils; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class MulTagSubIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(TagMessageWith1ConsumerIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + String consumerId = initConsumerGroup(); + logger.info(String.format("use topic: %s; consumerId: %s !", topic, consumerId)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testSubTwoTabMessageOnsTag() { + String tag = "jueyin1"; + String subExpress = String.format("%s||jueyin2", tag); + int msgSize = 10; + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, subExpress, + new RMQNormalListener()); + producer.send(tag, msgSize); + Assert.assertEquals("Not all sent succeeded", msgSize, producer.getAllUndupMsgBody().size()); + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + } + + @Test + public void testSubTwoTabAndMatchOne() { + String tag1 = "jueyin1"; + String tag2 = "jueyin2"; + String subExpress = String.format("%s||noExistTag", tag2); + int msgSize = 10; + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, subExpress, + new RMQNormalListener()); + + producer.send(tag1, msgSize); + Assert.assertEquals("Not all sent succeeded", msgSize, producer.getAllUndupMsgBody().size()); + List tag2Msgs = MQMessageFactory.getRMQMessage(tag2, topic, msgSize); + producer.send(tag2Msgs); + Assert.assertEquals("Not all sent succeeded", msgSize * 2, producer.getAllUndupMsgBody().size()); + + consumer.getListener().waitForMessageConsume(MQMessageFactory.getMessageBody(tag2Msgs), + CONSUME_TIME); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(MQMessageFactory.getMessageBody(tag2Msgs)); + } + + @Test + public void testSubTwoTabAndMatchTwo() { + String[] tags = {"jueyin1", "jueyin2"}; + String subExpress = String.format("%s||%s", tags[0], tags[1]); + int msgSize = 10; + + TagMessage tagMessage = new TagMessage(tags, topic, msgSize); + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, subExpress, + new RMQNormalListener()); + + producer.send(tagMessage.getMixedTagMessages()); + Assert.assertEquals("Not all sent succeeded", msgSize * tags.length, + producer.getAllUndupMsgBody().size()); + + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(tagMessage.getAllTagMessageBody()); + } + + @Test + public void testSubThreeTabAndMatchTwo() { + String[] tags = {"jueyin1", "jueyin2", "jueyin3"}; + String subExpress = String.format("%s||%s", tags[0], tags[1]); + int msgSize = 10; + + TagMessage tagMessage = new TagMessage(tags, topic, msgSize); + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, subExpress, + new RMQNormalListener()); + + producer.send(tagMessage.getMixedTagMessages()); + Assert.assertEquals("Not all sent succeeded", msgSize * tags.length, + producer.getAllUndupMsgBody().size()); + + consumer.getListener().waitForMessageConsume( + tagMessage.getMessageBodyByTag(tags[0], tags[1]), CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())).containsExactlyElementsIn( + tagMessage.getMessageBodyByTag(tags[0], tags[1])); + } + + @Test + public void testNoMatch() { + String[] tags = {"jueyin1", "jueyin2", "jueyin3"}; + String subExpress = "no_match"; + int msgSize = 10; + + TagMessage tagMessage = new TagMessage(tags, topic, msgSize); + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, subExpress, + new RMQNormalListener()); + + producer.send(tagMessage.getMixedTagMessages()); + Assert.assertEquals("Not all sent succeeded", msgSize * tags.length, + producer.getAllUndupMsgBody().size()); + + TestUtils.waitForSeconds(5); + + assertThat(VerifyUtils + .getFilterdMessage(producer.getAllMsgBody(), consumer.getListener().getAllMsgBody()) + .size()).isEqualTo(0); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/tag/TagMessageWith1ConsumerIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/tag/TagMessageWith1ConsumerIT.java new file mode 100644 index 0000000..6a85952 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/tag/TagMessageWith1ConsumerIT.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.test.client.consumer.tag; + +import java.util.List; + +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.factory.MQMessageFactory; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class TagMessageWith1ConsumerIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(TagMessageWith1ConsumerIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + String consumerId = initConsumerGroup(); + logger.info(String.format("use topic: %s; consumerId: %s !", topic, consumerId)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testTagSmoke() { + String tag = "jueyin"; + int msgSize = 10; + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, tag, new RMQNormalListener()); + producer.send(tag, msgSize); + Assert.assertEquals("Not all are sent", msgSize, producer.getAllUndupMsgBody().size()); + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + } + + @Test + public void testSubAllMessageNoTag() { + String subExprress = "*"; + int msgSize = 10; + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, subExprress, + new RMQNormalListener()); + producer.send(msgSize); + Assert.assertEquals("Not all are sent", msgSize, producer.getAllUndupMsgBody().size()); + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + } + + @Test + public void testSubAllMessageWithTag() { + String tag = "jueyin"; + String subExpress = "*"; + int msgSize = 10; + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, subExpress, + new RMQNormalListener()); + producer.send(tag, msgSize); + Assert.assertEquals("Not all are sent", msgSize, producer.getAllUndupMsgBody().size()); + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + } + + @Test + public void testSubAllMessageWithNullTag() { + String tag = null; + String subExpress = "*"; + int msgSize = 10; + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, subExpress, + new RMQNormalListener()); + producer.send(tag, msgSize); + Assert.assertEquals("Not all are sent", msgSize, producer.getAllUndupMsgBody().size()); + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + } + + @Test + public void testSubNullWithTagNull() { + String tag = null; + String subExpress = null; + int msgSize = 10; + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, subExpress, + new RMQNormalListener()); + producer.send(tag, msgSize); + Assert.assertEquals("Not all are sent", msgSize, producer.getAllUndupMsgBody().size()); + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + } + + @Test + public void testSubAllWithKindsOfMessage() { + String tag1 = null; + String tag2 = "jueyin"; + String subExpress = "*"; + int msgSize = 10; + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, subExpress, + new RMQNormalListener()); + + List tag1Msgs = MQMessageFactory.getRMQMessage(tag1, topic, msgSize); + List tag2Msgs = MQMessageFactory.getRMQMessage(tag2, topic, msgSize); + + producer.send(tag1Msgs); + producer.send(tag2Msgs); + producer.send(10); + Assert.assertEquals("Not all are sent", msgSize * 3, producer.getAllUndupMsgBody().size()); + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + } + + @Test + public void testSubNullWithKindsOfMessage() { + String tag1 = null; + String tag2 = "jueyin"; + String subExpress = null; + int msgSize = 10; + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, subExpress, + new RMQNormalListener()); + + List tag1Msgs = MQMessageFactory.getRMQMessage(tag1, topic, msgSize); + List tag2Msgs = MQMessageFactory.getRMQMessage(tag2, topic, msgSize); + + producer.send(tag1Msgs); + producer.send(tag2Msgs); + Assert.assertEquals("Not all are sent", msgSize * 2, producer.getAllUndupMsgBody().size()); + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + } + + @Test + public void testSubTagWithKindsOfMessage() { + String tag1 = null; + String tag2 = "jueyin"; + String subExpress = tag2; + int msgSize = 10; + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, subExpress, + new RMQNormalListener()); + + List tag1Msgs = MQMessageFactory.getRMQMessage(tag1, topic, msgSize); + List tag2Msgs = MQMessageFactory.getRMQMessage(tag2, topic, msgSize); + + producer.send(tag1Msgs); + producer.send(tag2Msgs); + producer.send(10); + Assert.assertEquals("Not all are sent", msgSize * 3, producer.getAllUndupMsgBody().size()); + consumer.getListener().waitForMessageConsume(MQMessageFactory.getMessageBody(tag2Msgs), + CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(MQMessageFactory.getMessageBody(tag2Msgs)); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/tag/TagMessageWithMulConsumerIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/tag/TagMessageWithMulConsumerIT.java new file mode 100644 index 0000000..cd975d8 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/tag/TagMessageWithMulConsumerIT.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.test.client.consumer.tag; + +import java.util.Collection; +import java.util.List; + +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.factory.MQMessageFactory; +import org.apache.rocketmq.test.factory.TagMessage; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class TagMessageWithMulConsumerIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(TagMessageWith1ConsumerIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + String consumerId = initConsumerGroup(); + logger.info(String.format("use topic: %s; consumerId: %s !", topic, consumerId)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testSendTwoTag() { + String tag1 = "jueyin1"; + String tag2 = "jueyin2"; + int msgSize = 10; + RMQNormalConsumer consumerTag1 = getConsumer(NAMESRV_ADDR, topic, tag1, + new RMQNormalListener()); + RMQNormalConsumer consumerTag2 = getConsumer(NAMESRV_ADDR, topic, tag2, + new RMQNormalListener()); + + List tag1Msgs = MQMessageFactory.getRMQMessage(tag1, topic, msgSize); + producer.send(tag1Msgs); + Assert.assertEquals("Not all are sent", msgSize, producer.getAllUndupMsgBody().size()); + List tag2Msgs = MQMessageFactory.getRMQMessage(tag2, topic, msgSize); + producer.send(tag2Msgs); + Assert.assertEquals("Not all are sent", msgSize * 2, producer.getAllUndupMsgBody().size()); + + consumerTag1.getListener().waitForMessageConsume(MQMessageFactory.getMessageBody(tag1Msgs), + CONSUME_TIME); + consumerTag2.getListener().waitForMessageConsume(MQMessageFactory.getMessageBody(tag2Msgs), + CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumerTag1.getListener().getAllMsgBody())) + .containsExactlyElementsIn(MQMessageFactory.getMessageBody(tag1Msgs)); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumerTag2.getListener().getAllMsgBody())) + .containsExactlyElementsIn(MQMessageFactory.getMessageBody(tag2Msgs)); + } + + @Test + public void testSendMessagesWithTwoTag() { + String[] tags = {"jueyin1", "jueyin2"}; + int msgSize = 10; + + TagMessage tagMessage = new TagMessage(tags, topic, msgSize); + RMQNormalConsumer consumerTag1 = getConsumer(NAMESRV_ADDR, topic, tags[0], + new RMQNormalListener()); + RMQNormalConsumer consumerTag2 = getConsumer(NAMESRV_ADDR, topic, tags[1], + new RMQNormalListener()); + + List tagMsgs = tagMessage.getMixedTagMessages(); + producer.send(tagMsgs); + Assert.assertEquals("Not all are sent", msgSize * tags.length, + producer.getAllUndupMsgBody().size()); + + consumerTag1.getListener().waitForMessageConsume(tagMessage.getMessageBodyByTag(tags[0]), + CONSUME_TIME); + consumerTag2.getListener().waitForMessageConsume(tagMessage.getMessageBodyByTag(tags[1]), + CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumerTag1.getListener().getAllMsgBody())) + .containsExactlyElementsIn(tagMessage.getMessageBodyByTag(tags[0])); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumerTag2.getListener().getAllMsgBody())) + .containsExactlyElementsIn(tagMessage.getMessageBodyByTag(tags[1])); + } + + @Test + public void testTwoConsumerOneMatchOneOtherMatchAll() { + String[] tags = {"jueyin1", "jueyin2"}; + String sub1 = String.format("%s||%s", tags[0], tags[1]); + String sub2 = String.format("%s|| noExist", tags[0]); + int msgSize = 10; + + TagMessage tagMessage = new TagMessage(tags, topic, msgSize); + RMQNormalConsumer consumerTag1 = getConsumer(NAMESRV_ADDR, topic, sub1, + new RMQNormalListener()); + RMQNormalConsumer consumerTag2 = getConsumer(NAMESRV_ADDR, topic, sub2, + new RMQNormalListener()); + + List tagMsgs = tagMessage.getMixedTagMessages(); + producer.send(tagMsgs); + Assert.assertEquals("Not all are sent", msgSize * tags.length, + producer.getAllUndupMsgBody().size()); + + consumerTag1.getListener().waitForMessageConsume(tagMessage.getMessageBodyByTag(tags), + CONSUME_TIME); + consumerTag2.getListener().waitForMessageConsume(tagMessage.getMessageBodyByTag(tags[0]), + CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumerTag1.getListener().getAllMsgBody())) + .containsExactlyElementsIn(tagMessage.getAllTagMessageBody()); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumerTag2.getListener().getAllMsgBody())) + .containsExactlyElementsIn(tagMessage.getMessageBodyByTag(tags[0])); + } + + @Test + public void testSubKindsOf() { + String[] tags = {"jueyin1", "jueyin2"}; + String sub1 = String.format("%s||%s", tags[0], tags[1]); + String sub2 = String.format("%s|| noExist", tags[0]); + String sub3 = tags[0]; + String sub4 = "*"; + int msgSize = 10; + + RMQNormalConsumer consumerSubTwoMatchAll = getConsumer(NAMESRV_ADDR, topic, sub1, + new RMQNormalListener()); + RMQNormalConsumer consumerSubTwoMachieOne = getConsumer(NAMESRV_ADDR, topic, sub2, + new RMQNormalListener()); + RMQNormalConsumer consumerSubTag1 = getConsumer(NAMESRV_ADDR, topic, sub3, + new RMQNormalListener()); + RMQNormalConsumer consumerSubAll = getConsumer(NAMESRV_ADDR, topic, sub4, + new RMQNormalListener()); + + producer.send(msgSize); + Assert.assertEquals("Not all are sent", msgSize, producer.getAllUndupMsgBody().size()); + Collection msgsWithNoTag = producer.getMsgBodysCopy(); + + TagMessage tagMessage = new TagMessage(tags, topic, msgSize); + List tagMsgs = tagMessage.getMixedTagMessages(); + producer.send(tagMsgs); + Assert.assertEquals("Not all are sent", msgSize * 3, producer.getAllUndupMsgBody().size()); + + consumerSubTwoMatchAll.getListener() + .waitForMessageConsume(tagMessage.getMessageBodyByTag(tags), CONSUME_TIME); + consumerSubTwoMachieOne.getListener() + .waitForMessageConsume(tagMessage.getMessageBodyByTag(tags[0]), CONSUME_TIME); + consumerSubTag1.getListener().waitForMessageConsume(tagMessage.getMessageBodyByTag(tags[0]), + CONSUME_TIME); + consumerSubAll.getListener().waitForMessageConsume( + MQMessageFactory.getMessage(msgsWithNoTag, tagMessage.getAllTagMessageBody()), + CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumerSubTwoMatchAll.getListener().getAllMsgBody())) + .containsExactlyElementsIn(tagMessage.getAllTagMessageBody()); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumerSubTwoMachieOne.getListener().getAllMsgBody())) + .containsExactlyElementsIn(tagMessage.getMessageBodyByTag(tags[0])); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumerSubTag1.getListener().getAllMsgBody())) + .containsExactlyElementsIn(tagMessage.getMessageBodyByTag(tags[0])); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumerSubAll.getListener().getAllMsgBody())) + .containsExactlyElementsIn(MQMessageFactory.getMessage(msgsWithNoTag, + tagMessage.getAllTagMessageBody())); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/tag/TagMessageWithSameGroupConsumerIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/tag/TagMessageWithSameGroupConsumerIT.java new file mode 100644 index 0000000..7d13948 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/tag/TagMessageWithSameGroupConsumerIT.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.test.client.consumer.tag; + +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.RandomUtils; +import org.apache.rocketmq.test.util.TestUtils; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class TagMessageWithSameGroupConsumerIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(TagMessageWith1ConsumerIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + private String tag = "tag"; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("use topic: %s !", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testTwoConsumerWithSameGroup() { + int msgSize = 20; + String originMsgDCName = RandomUtils.getStringByUUID(); + String msgBodyDCName = RandomUtils.getStringByUUID(); + RMQNormalConsumer consumer1 = getConsumer(NAMESRV_ADDR, topic, tag, + new RMQNormalListener(originMsgDCName, msgBodyDCName)); + getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), tag, + new RMQNormalListener(originMsgDCName, msgBodyDCName)); + producer.send(tag, msgSize); + Assert.assertEquals("Not all are sent", msgSize, producer.getAllUndupMsgBody().size()); + consumer1.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer1.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + } + + @Test + public void testConsumerStartWithInterval() { + int msgSize = 100; + String originMsgDCName = RandomUtils.getStringByUUID(); + String msgBodyDCName = RandomUtils.getStringByUUID(); + + RMQNormalConsumer consumer1 = getConsumer(NAMESRV_ADDR, topic, tag, + new RMQNormalListener(originMsgDCName, msgBodyDCName)); + producer.send(tag, msgSize, 100); + TestUtils.waitForMoment(5); + getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), tag, + new RMQNormalListener(originMsgDCName, msgBodyDCName)); + TestUtils.waitForMoment(5); + + consumer1.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer1.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + } + + @Test + public void testConsumerStartTwoAndCrashOneAfterWhile() { + int msgSize = 100; + String originMsgDCName = RandomUtils.getStringByUUID(); + String msgBodyDCName = RandomUtils.getStringByUUID(); + + RMQNormalConsumer consumer1 = getConsumer(NAMESRV_ADDR, topic, tag, + new RMQNormalListener(originMsgDCName, msgBodyDCName)); + RMQNormalConsumer consumer2 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), tag, + new RMQNormalListener(originMsgDCName, msgBodyDCName)); + + producer.send(tag, msgSize, 100); + TestUtils.waitForMoment(5); + consumer2.shutdown(); + mqClients.remove(1); + TestUtils.waitForMoment(5); + + consumer1.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer1.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/topic/MulConsumerMulTopicIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/topic/MulConsumerMulTopicIT.java new file mode 100644 index 0000000..28da660 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/topic/MulConsumerMulTopicIT.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.test.client.consumer.topic; + +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.factory.MQMessageFactory; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.MQWait; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class MulConsumerMulTopicIT extends BaseConf { + private RMQNormalProducer producer = null; + + @Before + public void setUp() { + producer = getProducer(NAMESRV_ADDR, null); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testSynSendMessage() { + int msgSize = 10; + String topic1 = initTopic(); + String topic2 = initTopic(); + RMQNormalConsumer consumer1 = getConsumer(NAMESRV_ADDR, topic1, "*", new RMQNormalListener()); + consumer1.subscribe(topic2, "*"); + RMQNormalConsumer consumer2 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), topic1, + "*", new RMQNormalListener()); + consumer2.subscribe(topic2, "*"); + + producer.send(MQMessageFactory.getMsg(topic1, msgSize)); + producer.send(MQMessageFactory.getMsg(topic2, msgSize)); + Assert.assertEquals("Not all sent succeeded", msgSize * 2, producer.getAllUndupMsgBody().size()); + + boolean recvAll = MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), + consumer1.getListener(), consumer2.getListener()); + assertThat(recvAll).isEqualTo(true); + } + + @Test + public void testConsumeWithDiffTag() { + int msgSize = 10; + String topic1 = initTopic(); + String topic2 = initTopic(); + String tag = "jueyin_tag"; + RMQNormalConsumer consumer1 = getConsumer(NAMESRV_ADDR, topic1, "*", new RMQNormalListener()); + consumer1.subscribe(topic2, tag); + RMQNormalConsumer consumer2 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), topic1, + "*", new RMQNormalListener()); + consumer2.subscribe(topic2, tag); + + producer.send(MQMessageFactory.getMsg(topic1, msgSize)); + producer.send(MQMessageFactory.getMsg(topic2, msgSize, tag)); + Assert.assertEquals("Not all sent succeeded", msgSize * 2, producer.getAllUndupMsgBody().size()); + + boolean recvAll = MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), + consumer1.getListener(), consumer2.getListener()); + assertThat(recvAll).isEqualTo(true); + } + + @Test + public void testConsumeWithDiffTagAndFilter() { + int msgSize = 10; + String topic1 = initTopic(); + String topic2 = initTopic(); + String tag1 = "jueyin_tag_1"; + String tag2 = "jueyin_tag_2"; + RMQNormalConsumer consumer1 = getConsumer(NAMESRV_ADDR, topic1, "*", new RMQNormalListener()); + consumer1.subscribe(topic2, tag1); + RMQNormalConsumer consumer2 = getConsumer(NAMESRV_ADDR, topic1, "*", new RMQNormalListener()); + consumer2.subscribe(topic2, tag1); + + producer.send(MQMessageFactory.getMsg(topic2, msgSize, tag2)); + producer.clearMsg(); + producer.send(MQMessageFactory.getMsg(topic1, msgSize)); + producer.send(MQMessageFactory.getMsg(topic2, msgSize, tag1)); + + boolean recvAll = MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), + consumer1.getListener(), consumer2.getListener()); + assertThat(recvAll).isEqualTo(true); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/consumer/topic/OneConsumerMulTopicIT.java b/test/src/test/java/org/apache/rocketmq/test/client/consumer/topic/OneConsumerMulTopicIT.java new file mode 100644 index 0000000..a622ca5 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/consumer/topic/OneConsumerMulTopicIT.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.test.client.consumer.topic; + +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.factory.MQMessageFactory; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class OneConsumerMulTopicIT extends BaseConf { + private RMQNormalProducer producer = null; + + @Before + public void setUp() { + producer = getProducer(NAMESRV_ADDR, null); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testSynSendMessage() { + int msgSize = 10; + String topic1 = initTopic(); + String topic2 = initTopic(); + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic1, "*", new RMQNormalListener()); + consumer.subscribe(topic2, "*"); + + producer.send(MQMessageFactory.getMsg(topic1, msgSize)); + producer.send(MQMessageFactory.getMsg(topic2, msgSize)); + + Assert.assertEquals("Not all are sent", msgSize * 2, producer.getAllUndupMsgBody().size()); + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + } + + @Test + public void testConsumeWithDiffTag() { + int msgSize = 10; + String topic1 = initTopic(); + String topic2 = initTopic(); + String tag = "jueyin_tag"; + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic1, "*", new RMQNormalListener()); + consumer.subscribe(topic2, tag); + + producer.send(MQMessageFactory.getMsg(topic1, msgSize)); + producer.send(MQMessageFactory.getMsg(topic2, msgSize, tag)); + + Assert.assertEquals("Not all are sent", msgSize * 2, producer.getAllUndupMsgBody().size()); + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + } + + @Test + public void testConsumeWithDiffTagAndFilter() { + int msgSize = 10; + String topic1 = initTopic(); + String topic2 = initTopic(); + String tag1 = "jueyin_tag_1"; + String tag2 = "jueyin_tag_2"; + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic1, "*", new RMQNormalListener()); + consumer.subscribe(topic2, tag1); + + producer.send(MQMessageFactory.getMsg(topic2, msgSize, tag2)); + producer.clearMsg(); + producer.send(MQMessageFactory.getMsg(topic1, msgSize)); + producer.send(MQMessageFactory.getMsg(topic2, msgSize, tag1)); + + Assert.assertEquals("Not all are sent", msgSize * 2, producer.getAllUndupMsgBody().size()); + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/producer/async/AsyncSendExceptionIT.java b/test/src/test/java/org/apache/rocketmq/test/client/producer/async/AsyncSendExceptionIT.java new file mode 100644 index 0000000..930cdb8 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/producer/async/AsyncSendExceptionIT.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.test.client.producer.async; + +import java.util.List; +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.message.Message; +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.test.base.BaseConf; +import org.apache.rocketmq.test.client.consumer.tag.TagMessageWith1ConsumerIT; +import org.apache.rocketmq.test.factory.ProducerFactory; +import org.apache.rocketmq.test.factory.SendCallBackFactory; +import org.apache.rocketmq.test.util.RandomUtils; +import org.apache.rocketmq.test.util.TestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class AsyncSendExceptionIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(TagMessageWith1ConsumerIT.class); + private static boolean sendFail = false; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("user topic[%s]!", topic)); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testSendCallBackNull() throws Exception { + Message msg = new Message(topic, RandomUtils.getStringByUUID().getBytes()); + DefaultMQProducer producer = ProducerFactory.getRMQProducer(NAMESRV_ADDR); + SendCallback sendCallback = null; + producer.send(msg, sendCallback); + } + + @Test + public void testSendMQNull() throws Exception { + Message msg = new Message(topic, RandomUtils.getStringByUUID().getBytes()); + DefaultMQProducer producer = ProducerFactory.getRMQProducer(NAMESRV_ADDR); + MessageQueue messageQueue = null; + producer.send(msg, messageQueue, SendCallBackFactory.getSendCallBack()); + } + + @Test + public void testSendSelectorNull() throws Exception { + Message msg = new Message(topic, RandomUtils.getStringByUUID().getBytes()); + DefaultMQProducer producer = ProducerFactory.getRMQProducer(NAMESRV_ADDR); + MessageQueueSelector selector = null; + producer.send(msg, selector, 100, SendCallBackFactory.getSendCallBack()); + } + + @Test + public void testSelectorThrowsException() throws Exception { + Message msg = new Message(topic, RandomUtils.getStringByUUID().getBytes()); + DefaultMQProducer producer = ProducerFactory.getRMQProducer(NAMESRV_ADDR); + producer.send(msg, new MessageQueueSelector() { + @Override + public MessageQueue select(List list, Message message, Object o) { + String str = null; + return list.get(str.length()); + } + }, null, SendCallBackFactory.getSendCallBack()); + } + + @Test + public void testQueueIdBigThanQueueNum() throws Exception { + int queueId = 100; + sendFail = false; + MessageQueue mq = new MessageQueue(topic, BROKER1_NAME, queueId); + Message msg = new Message(topic, RandomUtils.getStringByUUID().getBytes()); + DefaultMQProducer producer = ProducerFactory.getRMQProducer(NAMESRV_ADDR); + + producer.send(msg, mq, new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + } + + @Override + public void onException(Throwable throwable) { + sendFail = true; + } + }); + + int checkNum = 50; + while (!sendFail && checkNum > 0) { + checkNum--; + TestUtils.waitForMoment(100); + } + producer.shutdown(); + assertThat(sendFail).isEqualTo(true); + } + + @Test + public void testQueueIdSmallZero() throws Exception { + int queueId = -100; + sendFail = true; + MessageQueue mq = new MessageQueue(topic, BROKER1_NAME, queueId); + Message msg = new Message(topic, RandomUtils.getStringByUUID().getBytes()); + DefaultMQProducer producer = ProducerFactory.getRMQProducer(NAMESRV_ADDR); + + producer.send(msg, mq, new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + sendFail = false; + } + + @Override + public void onException(Throwable throwable) { + sendFail = true; + } + }); + + int checkNum = 50; + while (sendFail && checkNum > 0) { + checkNum--; + TestUtils.waitForMoment(100); + } + producer.shutdown(); + assertThat(sendFail).isEqualTo(false); + } + +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/producer/async/AsyncSendWithMessageQueueIT.java b/test/src/test/java/org/apache/rocketmq/test/client/producer/async/AsyncSendWithMessageQueueIT.java new file mode 100644 index 0000000..c665926 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/producer/async/AsyncSendWithMessageQueueIT.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.test.client.producer.async; + +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.test.base.BaseConf; +import org.apache.rocketmq.test.client.consumer.tag.TagMessageWith1ConsumerIT; +import org.apache.rocketmq.test.client.rmq.RMQAsyncSendProducer; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class AsyncSendWithMessageQueueIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(TagMessageWith1ConsumerIT.class); + private RMQAsyncSendProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("user topic[%s]!", topic)); + producer = getAsyncProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testAsyncSendWithMQ() { + int msgSize = 20; + int queueId = 0; + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + MessageQueue mq = new MessageQueue(topic, BROKER1_NAME, queueId); + + producer.asyncSend(msgSize, mq); + producer.waitForResponse(10 * 1000); + assertThat(producer.getSuccessMsgCount()).isEqualTo(msgSize); + + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + + VerifyUtils.verifyMessageQueueId(queueId, consumer.getListener().getAllOriginMsg()); + + producer.clearMsg(); + consumer.clearMsg(); + producer.getSuccessSendResult().clear(); + mq = new MessageQueue(topic, BROKER2_NAME, queueId); + producer.asyncSend(msgSize, mq); + producer.waitForResponse(10 * 1000); + assertThat(producer.getSuccessMsgCount()).isEqualTo(msgSize); + + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + + VerifyUtils.verifyMessageQueueId(queueId, consumer.getListener().getAllOriginMsg()); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/producer/async/AsyncSendWithMessageQueueSelectorIT.java b/test/src/test/java/org/apache/rocketmq/test/client/producer/async/AsyncSendWithMessageQueueSelectorIT.java new file mode 100644 index 0000000..add0796 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/producer/async/AsyncSendWithMessageQueueSelectorIT.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.test.client.producer.async; + +import java.util.List; +import org.apache.rocketmq.client.producer.MessageQueueSelector; +import org.apache.rocketmq.common.message.Message; +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.test.base.BaseConf; +import org.apache.rocketmq.test.client.consumer.tag.TagMessageWith1ConsumerIT; +import org.apache.rocketmq.test.client.rmq.RMQAsyncSendProducer; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class AsyncSendWithMessageQueueSelectorIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(TagMessageWith1ConsumerIT.class); + private RMQAsyncSendProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("user topic[%s]!", topic)); + producer = getAsyncProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testSendWithSelector() { + int msgSize = 20; + final int queueId = 0; + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + + producer.asyncSend(msgSize, new MessageQueueSelector() { + @Override + public MessageQueue select(List list, Message message, Object o) { + for (MessageQueue mq : list) { + if (mq.getQueueId() == queueId && mq.getBrokerName().equals(BROKER1_NAME)) { + return mq; + } + } + return list.get(0); + } + }); + producer.waitForResponse(5 * 1000); + assertThat(producer.getSuccessMsgCount()).isEqualTo(msgSize); + + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + + VerifyUtils.verifyMessageQueueId(queueId, consumer.getListener().getAllOriginMsg()); + + producer.clearMsg(); + consumer.clearMsg(); + producer.getSuccessSendResult().clear(); + + producer.asyncSend(msgSize, new MessageQueueSelector() { + @Override + public MessageQueue select(List list, Message message, Object o) { + for (MessageQueue mq : list) { + if (mq.getQueueId() == queueId && mq.getBrokerName().equals(BROKER2_NAME)) { + return mq; + } + } + return list.get(8); + } + }); + producer.waitForResponse(5 * 1000); + assertThat(producer.getSuccessMsgCount()).isEqualTo(msgSize); + + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + + VerifyUtils.verifyMessageQueueId(queueId, consumer.getListener().getAllOriginMsg()); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/producer/async/AsyncSendWithOnlySendCallBackIT.java b/test/src/test/java/org/apache/rocketmq/test/client/producer/async/AsyncSendWithOnlySendCallBackIT.java new file mode 100644 index 0000000..b621a24 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/producer/async/AsyncSendWithOnlySendCallBackIT.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.test.client.producer.async; + +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.consumer.tag.TagMessageWith1ConsumerIT; +import org.apache.rocketmq.test.client.rmq.RMQAsyncSendProducer; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class AsyncSendWithOnlySendCallBackIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(TagMessageWith1ConsumerIT.class); + private RMQAsyncSendProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("user topic[%s]!", topic)); + producer = getAsyncProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testSendWithOnlyCallBack() { + int msgSize = 20; + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + producer.asyncSend(msgSize); + producer.waitForResponse(10 * 1000); + assertThat(producer.getSuccessMsgCount()).isEqualTo(msgSize); + + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + } + +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/producer/batch/BatchSendIT.java b/test/src/test/java/org/apache/rocketmq/test/client/producer/batch/BatchSendIT.java new file mode 100644 index 0000000..38176c8 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/producer/batch/BatchSendIT.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.test.client.producer.batch; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.UUID; + +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.consumer.PullStatus; +import org.apache.rocketmq.client.hook.SendMessageContext; +import org.apache.rocketmq.client.hook.SendMessageHook; +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.TopicAttributes; +import org.apache.rocketmq.common.attribute.CQType; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageBatch; +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.store.DefaultMessageStore; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.base.IntegrationTestBase; +import org.apache.rocketmq.test.client.consumer.tag.TagMessageWith1ConsumerIT; +import org.apache.rocketmq.test.factory.ConsumerFactory; +import org.apache.rocketmq.test.factory.ProducerFactory; +import org.apache.rocketmq.test.util.RandomUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class BatchSendIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(TagMessageWith1ConsumerIT.class); + private String topic = null; + private Random random = new Random(); + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("user topic[%s]!", topic)); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testBatchSend_ViewMessage() throws Exception { + Assert.assertTrue(brokerController1.getMessageStore() instanceof DefaultMessageStore); + Assert.assertTrue(brokerController2.getMessageStore() instanceof DefaultMessageStore); + + List messageList = new ArrayList<>(); + int batchNum = 100; + for (int i = 0; i < batchNum; i++) { + messageList.add(new Message(topic, RandomUtils.getStringByUUID().getBytes())); + } + + DefaultMQProducer producer = ProducerFactory.getRMQProducer(NAMESRV_ADDR); + removeBatchUniqueId(producer); + + SendResult sendResult = producer.send(messageList); + Assert.assertEquals(SendStatus.SEND_OK, sendResult.getSendStatus()); + + String[] offsetIds = sendResult.getOffsetMsgId().split(","); + String[] msgIds = sendResult.getMsgId().split(","); + Assert.assertEquals(messageList.size(), offsetIds.length); + Assert.assertEquals(messageList.size(), msgIds.length); + + Thread.sleep(2000); + + for (int i = 0; i < 3; i++) { + producer.viewMessage(topic, offsetIds[random.nextInt(batchNum)]); + } + for (int i = 0; i < 3; i++) { + producer.viewMessage(topic, msgIds[random.nextInt(batchNum)]); + } + } + + @Test + public void testBatchSend_SysInnerBatch() throws Exception { + waitBrokerRegistered(NAMESRV_ADDR, CLUSTER_NAME, BROKER_NUM); + + String batchTopic = UUID.randomUUID().toString(); + IntegrationTestBase.initTopic(batchTopic, NAMESRV_ADDR, CLUSTER_NAME, CQType.BatchCQ); + + Assert.assertEquals(CQType.BatchCQ.toString(), brokerController1.getTopicConfigManager().getTopicConfigTable().get(batchTopic).getAttributes().get(TopicAttributes.QUEUE_TYPE_ATTRIBUTE.getName())); + Assert.assertEquals(CQType.BatchCQ.toString(), brokerController2.getTopicConfigManager().getTopicConfigTable().get(batchTopic).getAttributes().get(TopicAttributes.QUEUE_TYPE_ATTRIBUTE.getName())); + Assert.assertEquals(CQType.BatchCQ.toString(), brokerController3.getTopicConfigManager().getTopicConfigTable().get(batchTopic).getAttributes().get(TopicAttributes.QUEUE_TYPE_ATTRIBUTE.getName())); + Assert.assertEquals(8, brokerController1.getTopicConfigManager().getTopicConfigTable().get(batchTopic).getReadQueueNums()); + Assert.assertEquals(8, brokerController2.getTopicConfigManager().getTopicConfigTable().get(batchTopic).getReadQueueNums()); + Assert.assertEquals(8, brokerController3.getTopicConfigManager().getTopicConfigTable().get(batchTopic).getReadQueueNums()); + Assert.assertEquals(-1, brokerController1.getMessageStore().getMinOffsetInQueue(batchTopic, 0)); + Assert.assertEquals(-1, brokerController2.getMessageStore().getMinOffsetInQueue(batchTopic, 0)); + Assert.assertEquals(-1, brokerController3.getMessageStore().getMinOffsetInQueue(batchTopic, 0)); + Assert.assertEquals(0, brokerController1.getMessageStore().getMaxOffsetInQueue(batchTopic, 0)); + Assert.assertEquals(0, brokerController2.getMessageStore().getMaxOffsetInQueue(batchTopic, 0)); + Assert.assertEquals(0, brokerController3.getMessageStore().getMaxOffsetInQueue(batchTopic, 0)); + + DefaultMQProducer producer = ProducerFactory.getRMQProducer(NAMESRV_ADDR); + MessageQueue messageQueue = producer.fetchPublishMessageQueues(batchTopic).iterator().next(); + + int batchCount = 10; + int batchNum = 10; + for (int i = 0; i < batchCount; i++) { + List messageList = new ArrayList<>(); + for (int j = 0; j < batchNum; j++) { + messageList.add(new Message(batchTopic, RandomUtils.getStringByUUID().getBytes())); + } + SendResult sendResult = producer.send(messageList, messageQueue); + Assert.assertEquals(SendStatus.SEND_OK, sendResult.getSendStatus()); + Assert.assertEquals(messageQueue.getQueueId(), sendResult.getMessageQueue().getQueueId()); + Assert.assertEquals(i * batchNum, sendResult.getQueueOffset()); + Assert.assertEquals(1, sendResult.getMsgId().split(",").length); + } + Thread.sleep(300); + { + DefaultMQPullConsumer defaultMQPullConsumer = ConsumerFactory.getRMQPullConsumer(NAMESRV_ADDR, "group"); + + PullResult pullResult = defaultMQPullConsumer.pullBlockIfNotFound(messageQueue, "*", 5, batchCount * batchNum); + Assert.assertEquals(PullStatus.FOUND, pullResult.getPullStatus()); + Assert.assertEquals(0, pullResult.getMinOffset()); + Assert.assertEquals(batchCount * batchNum, pullResult.getMaxOffset()); + Assert.assertEquals(batchCount * batchNum, pullResult.getMsgFoundList().size()); + MessageExt first = pullResult.getMsgFoundList().get(0); + for (int i = 0; i < pullResult.getMsgFoundList().size(); i++) { + MessageExt messageExt = pullResult.getMsgFoundList().get(i); + if (i % batchNum == 0) { + first = messageExt; + } + Assert.assertEquals(i, messageExt.getQueueOffset()); + Assert.assertEquals(batchTopic, messageExt.getTopic()); + Assert.assertEquals(messageQueue.getQueueId(), messageExt.getQueueId()); + Assert.assertEquals(first.getBornHostString(), messageExt.getBornHostString()); + Assert.assertEquals(first.getBornHostNameString(), messageExt.getBornHostNameString()); + Assert.assertEquals(first.getBornTimestamp(), messageExt.getBornTimestamp()); + Assert.assertEquals(first.getStoreTimestamp(), messageExt.getStoreTimestamp()); + } + } + } + + @Test + public void testBatchSend_SysOuterBatch() throws Exception { + Assert.assertTrue(brokerController1.getMessageStore() instanceof DefaultMessageStore); + Assert.assertTrue(brokerController2.getMessageStore() instanceof DefaultMessageStore); + Assert.assertTrue(brokerController3.getMessageStore() instanceof DefaultMessageStore); + + String batchTopic = UUID.randomUUID().toString(); + IntegrationTestBase.initTopic(batchTopic, NAMESRV_ADDR, CLUSTER_NAME, CQType.SimpleCQ); + Assert.assertEquals(8, brokerController1.getTopicConfigManager().getTopicConfigTable().get(batchTopic).getReadQueueNums()); + Assert.assertEquals(8, brokerController2.getTopicConfigManager().getTopicConfigTable().get(batchTopic).getReadQueueNums()); + Assert.assertEquals(8, brokerController3.getTopicConfigManager().getTopicConfigTable().get(batchTopic).getReadQueueNums()); + Assert.assertEquals(0, brokerController1.getMessageStore().getMinOffsetInQueue(batchTopic, 0)); + Assert.assertEquals(0, brokerController2.getMessageStore().getMinOffsetInQueue(batchTopic, 0)); + Assert.assertEquals(0, brokerController3.getMessageStore().getMinOffsetInQueue(batchTopic, 0)); + Assert.assertEquals(0, brokerController1.getMessageStore().getMaxOffsetInQueue(batchTopic, 0)); + Assert.assertEquals(0, brokerController2.getMessageStore().getMaxOffsetInQueue(batchTopic, 0)); + Assert.assertEquals(0, brokerController3.getMessageStore().getMaxOffsetInQueue(batchTopic, 0)); + + DefaultMQProducer producer = ProducerFactory.getRMQProducer(NAMESRV_ADDR); + MessageQueue messageQueue = producer.fetchPublishMessageQueues(batchTopic).iterator().next(); + + int batchCount = 10; + int batchNum = 10; + for (int i = 0; i < batchCount; i++) { + List messageList = new ArrayList<>(); + for (int j = 0; j < batchNum; j++) { + messageList.add(new Message(batchTopic, RandomUtils.getStringByUUID().getBytes())); + } + SendResult sendResult = producer.send(messageList, messageQueue); + Assert.assertEquals(SendStatus.SEND_OK, sendResult.getSendStatus()); + Assert.assertEquals(messageQueue.getQueueId(), sendResult.getMessageQueue().getQueueId()); + Assert.assertEquals(i * batchNum, sendResult.getQueueOffset()); + Assert.assertEquals(10, sendResult.getMsgId().split(",").length); + } + Thread.sleep(300); + { + DefaultMQPullConsumer defaultMQPullConsumer = ConsumerFactory.getRMQPullConsumer(NAMESRV_ADDR, "group"); + + long startOffset = 5; + PullResult pullResult = defaultMQPullConsumer.pullBlockIfNotFound(messageQueue, "*", startOffset, batchCount * batchNum); + Assert.assertEquals(PullStatus.FOUND, pullResult.getPullStatus()); + Assert.assertEquals(0, pullResult.getMinOffset()); + Assert.assertEquals(batchCount * batchNum, pullResult.getMaxOffset()); + Assert.assertEquals(batchCount * batchNum - startOffset, pullResult.getMsgFoundList().size()); + for (int i = 0; i < pullResult.getMsgFoundList().size(); i++) { + MessageExt messageExt = pullResult.getMsgFoundList().get(i); + Assert.assertEquals(i + startOffset, messageExt.getQueueOffset()); + Assert.assertEquals(batchTopic, messageExt.getTopic()); + Assert.assertEquals(messageQueue.getQueueId(), messageExt.getQueueId()); + } + } + } + + @Test + public void testBatchSend_CheckProperties() throws Exception { + List messageList = new ArrayList<>(); + Message message = new Message(); + message.setTopic(topic); + message.setKeys("keys123"); + message.setTags("tags123"); + message.setWaitStoreMsgOK(false); + message.setBuyerId("buyerid123"); + message.setFlag(123); + message.setBody("body".getBytes()); + messageList.add(message); + + DefaultMQProducer producer = ProducerFactory.getRMQProducer(NAMESRV_ADDR); + removeBatchUniqueId(producer); + + SendResult sendResult = producer.send(messageList); + Assert.assertEquals(SendStatus.SEND_OK, sendResult.getSendStatus()); + + String[] offsetIds = sendResult.getOffsetMsgId().split(","); + String[] msgIds = sendResult.getMsgId().split(","); + Assert.assertEquals(messageList.size(), offsetIds.length); + Assert.assertEquals(messageList.size(), msgIds.length); + + Thread.sleep(2000); + + Message messageByOffset = producer.viewMessage(topic, offsetIds[0]); + Message messageByMsgId = producer.viewMessage(topic, msgIds[0]); + + Assert.assertEquals(message.getTopic(), messageByMsgId.getTopic()); + Assert.assertEquals(message.getTopic(), messageByOffset.getTopic()); + + Assert.assertEquals(message.getKeys(), messageByOffset.getKeys()); + Assert.assertEquals(message.getKeys(), messageByMsgId.getKeys()); + + Assert.assertEquals(message.getTags(), messageByOffset.getTags()); + Assert.assertEquals(message.getTags(), messageByMsgId.getTags()); + + Assert.assertEquals(message.isWaitStoreMsgOK(), messageByOffset.isWaitStoreMsgOK()); + Assert.assertEquals(message.isWaitStoreMsgOK(), messageByMsgId.isWaitStoreMsgOK()); + + Assert.assertEquals(message.getBuyerId(), messageByOffset.getBuyerId()); + Assert.assertEquals(message.getBuyerId(), messageByMsgId.getBuyerId()); + + Assert.assertEquals(message.getFlag(), messageByOffset.getFlag()); + Assert.assertEquals(message.getFlag(), messageByMsgId.getFlag()); + } + + // simulate legacy batch message send. + private void removeBatchUniqueId(DefaultMQProducer producer) { + producer.getDefaultMQProducerImpl().registerSendMessageHook(new SendMessageHook() { + @Override + public String hookName() { + return null; + } + + @Override + public void sendMessageBefore(SendMessageContext context) { + MessageBatch messageBatch = (MessageBatch) context.getMessage(); + if (messageBatch.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX) != null) { + messageBatch.getProperties().remove(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX); + } + } + + @Override + public void sendMessageAfter(SendMessageContext context) { + } + }); + } + +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/producer/exception/msg/ChinaPropIT.java b/test/src/test/java/org/apache/rocketmq/test/client/producer/exception/msg/ChinaPropIT.java new file mode 100644 index 0000000..82aed46 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/producer/exception/msg/ChinaPropIT.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.test.client.producer.exception.msg; + +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.test.base.BaseConf; +import org.apache.rocketmq.test.factory.MessageFactory; +import org.apache.rocketmq.test.factory.ProducerFactory; +import org.apache.rocketmq.test.util.RandomUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class ChinaPropIT extends BaseConf { + private static DefaultMQProducer producer = null; + private static String topic = null; + + @Before + public void setUp() { + producer = ProducerFactory.getRMQProducer(NAMESRV_ADDR); + topic = initTopic(); + } + + @After + public void tearDown() { + producer.shutdown(); + } + + /** + * @since version3.4.6 + */ + @Test(expected = org.apache.rocketmq.client.exception.MQBrokerException.class) + public void testSend20kChinaPropMsg() throws Exception { + Message msg = MessageFactory.getRandomMessage(topic); + msg.putUserProperty("key", RandomUtils.getCheseWord(32 * 1024 + 1)); + producer.send(msg); + } + + /** + * @since version3.4.6 + */ + @Test + public void testSend10kChinaPropMsg() { + + Message msg = MessageFactory.getRandomMessage(topic); + msg.putUserProperty("key", RandomUtils.getCheseWord(10 * 1024)); + SendResult sendResult = null; + try { + sendResult = producer.send(msg); + } catch (Exception e) { + } + assertThat(sendResult.getSendStatus()).isEqualTo(SendStatus.SEND_OK); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/producer/exception/msg/MessageExceptionIT.java b/test/src/test/java/org/apache/rocketmq/test/client/producer/exception/msg/MessageExceptionIT.java new file mode 100644 index 0000000..cbd2cff --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/producer/exception/msg/MessageExceptionIT.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.test.client.producer.exception.msg; + +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.test.base.BaseConf; +import org.apache.rocketmq.test.factory.MessageFactory; +import org.apache.rocketmq.test.factory.ProducerFactory; +import org.apache.rocketmq.test.util.RandomUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class MessageExceptionIT extends BaseConf { + private static DefaultMQProducer producer = null; + private static String topic = null; + + @Before + public void setUp() { + producer = ProducerFactory.getRMQProducer(NAMESRV_ADDR); + topic = initTopic(); + } + + @After + public void tearDown() { + producer.shutdown(); + } + + @Test + public void testProducerSmoke() { + Message msg = new Message(topic, RandomUtils.getStringByUUID().getBytes()); + SendResult sendResult = null; + try { + sendResult = producer.send(msg); + } catch (Exception e) { + } + + assertThat(sendResult).isNotEqualTo(null); + assertThat(sendResult.getSendStatus()).isEqualTo(SendStatus.SEND_OK); + } + + @Test(expected = java.lang.NullPointerException.class) + public void testSynSendNullMessage() throws Exception { + producer.send((Message) null); + } + + @Test(expected = org.apache.rocketmq.client.exception.MQClientException.class) + public void testSynSendNullBodyMessage() throws Exception { + Message msg = new Message(topic, RandomUtils.getStringByUUID().getBytes()); + msg.setBody(null); + producer.send(msg); + } + + @Test(expected = org.apache.rocketmq.client.exception.MQClientException.class) + public void testSynSendZeroSizeBodyMessage() throws Exception { + Message msg = new Message(topic, RandomUtils.getStringByUUID().getBytes()); + msg.setBody(new byte[0]); + producer.send(msg); + } + + @Test(expected = org.apache.rocketmq.client.exception.MQClientException.class) + public void testSynSendOutOfSizeBodyMessage() throws Exception { + Message msg = new Message(topic, RandomUtils.getStringByUUID().getBytes()); + msg.setBody(new byte[1024 * 1024 * 4 + 1]); + producer.send(msg); + } + + @Test(expected = org.apache.rocketmq.client.exception.MQClientException.class) + public void testSynSendNullTopicMessage() throws Exception { + Message msg = new Message(null, RandomUtils.getStringByUUID().getBytes()); + producer.send(msg); + } + + @Test(expected = org.apache.rocketmq.client.exception.MQClientException.class) + public void testSynSendBlankTopicMessage() throws Exception { + Message msg = new Message("", RandomUtils.getStringByUUID().getBytes()); + producer.send(msg); + } + + @Test(expected = org.apache.rocketmq.client.exception.MQClientException.class) + public void testSend128kMsg() throws Exception { + Message msg = new Message(topic, + RandomUtils.getStringWithNumber(1024 * 1024 * 4 + 1).getBytes()); + producer.send(msg); + } + + @Test + public void testSendLess128kMsg() { + Message msg = new Message(topic, RandomUtils.getStringWithNumber(128 * 1024).getBytes()); + SendResult sendResult = null; + try { + sendResult = producer.send(msg); + } catch (Exception e) { + } + assertThat(sendResult.getSendStatus()).isEqualTo(SendStatus.SEND_OK); + } + + @Test + public void testSendMsgWithUserProperty() { + Message msg = MessageFactory.getRandomMessage(topic); + msg.putUserProperty("key", RandomUtils.getCheseWord(10 * 1024)); + SendResult sendResult = null; + try { + sendResult = producer.send(msg); + } catch (Exception e) { + } + assertThat(sendResult.getSendStatus()).isEqualTo(SendStatus.SEND_OK); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/producer/exception/msg/MessageUserPropIT.java b/test/src/test/java/org/apache/rocketmq/test/client/producer/exception/msg/MessageUserPropIT.java new file mode 100644 index 0000000..60ec1b1 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/producer/exception/msg/MessageUserPropIT.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.test.client.producer.exception.msg; + +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.consumer.balance.NormalMsgStaticBalanceIT; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.factory.MessageFactory; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class MessageUserPropIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(NormalMsgStaticBalanceIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("use topic: %s !", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + /** + * @since version3.4.6 + */ + @Test + public void testSendEnglishUserProp() { + Message msg = MessageFactory.getRandomMessage(topic); + String msgKey = "jueyinKey"; + String msgValue = "jueyinValue"; + msg.putUserProperty(msgKey, msgValue); + + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + + producer.send(msg, null); + assertThat(producer.getAllMsgBody().size()).isEqualTo(1); + + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + Message sendMsg = (Message) producer.getFirstMsg(); + Message recvMsg = (Message) consumer.getListener().getFirstMsg(); + assertThat(recvMsg.getUserProperty(msgKey)).isEqualTo(sendMsg.getUserProperty(msgKey)); + } + + /** + * @since version3.4.6 + */ + @Test + public void testSendChinaUserProp() { + Message msg = MessageFactory.getRandomMessage(topic); + String msgKey = "jueyinKey"; + String msgValue = "jueyinzhi"; + msg.putUserProperty(msgKey, msgValue); + + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + + producer.send(msg, null); + assertThat(producer.getAllMsgBody().size()).isEqualTo(1); + + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + Message sendMsg = (Message) producer.getFirstMsg(); + Message recvMsg = (Message) consumer.getListener().getFirstMsg(); + assertThat(recvMsg.getUserProperty(msgKey)).isEqualTo(sendMsg.getUserProperty(msgKey)); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/producer/exception/producer/ProducerGroupAndInstanceNameValidityIT.java b/test/src/test/java/org/apache/rocketmq/test/client/producer/exception/producer/ProducerGroupAndInstanceNameValidityIT.java new file mode 100644 index 0000000..0568da6 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/producer/exception/producer/ProducerGroupAndInstanceNameValidityIT.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.test.client.producer.exception.producer; + +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.util.RandomUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class ProducerGroupAndInstanceNameValidityIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(ProducerGroupAndInstanceNameValidityIT.class); + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("use topic: %s !", topic)); + } + + @After + public void tearDown() { + super.shutdown(); + } + + /** + * @since version3.4.6 + */ + @Test + public void testTwoProducerSameGroupAndInstanceName() { + RMQNormalProducer producer1 = getProducer(NAMESRV_ADDR, topic); + assertThat(producer1.isStartSuccess()).isEqualTo(true); + RMQNormalProducer producer2 = getProducer(NAMESRV_ADDR, topic, + producer1.getProducerGroupName(), producer1.getProducerInstanceName()); + assertThat(producer2.isStartSuccess()).isEqualTo(false); + } + + /** + * @since version3.4.6 + */ + @Test + public void testTwoProducerSameGroup() { + RMQNormalProducer producer1 = getProducer(NAMESRV_ADDR, topic); + assertThat(producer1.isStartSuccess()).isEqualTo(true); + RMQNormalProducer producer2 = getProducer(NAMESRV_ADDR, topic, + producer1.getProducerGroupName(), RandomUtils.getStringByUUID()); + assertThat(producer2.isStartSuccess()).isEqualTo(true); + } + +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/producer/oneway/OneWaySendExceptionIT.java b/test/src/test/java/org/apache/rocketmq/test/client/producer/oneway/OneWaySendExceptionIT.java new file mode 100644 index 0000000..6dbd025 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/producer/oneway/OneWaySendExceptionIT.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.test.client.producer.oneway; + +import java.util.List; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.MessageQueueSelector; +import org.apache.rocketmq.common.message.Message; +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.test.base.BaseConf; +import org.apache.rocketmq.test.client.consumer.tag.TagMessageWith1ConsumerIT; +import org.apache.rocketmq.test.factory.ProducerFactory; +import org.apache.rocketmq.test.util.RandomUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class OneWaySendExceptionIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(TagMessageWith1ConsumerIT.class); + private static boolean sendFail = false; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("user topic[%s]!", topic)); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test(expected = java.lang.NullPointerException.class) + public void testSendMQNull() throws Exception { + Message msg = new Message(topic, RandomUtils.getStringByUUID().getBytes()); + DefaultMQProducer producer = ProducerFactory.getRMQProducer(NAMESRV_ADDR); + MessageQueue messageQueue = null; + producer.sendOneway(msg, messageQueue); + } + + @Test(expected = org.apache.rocketmq.client.exception.MQClientException.class) + public void testSendSelectorNull() throws Exception { + Message msg = new Message(topic, RandomUtils.getStringByUUID().getBytes()); + DefaultMQProducer producer = ProducerFactory.getRMQProducer(NAMESRV_ADDR); + MessageQueueSelector selector = null; + producer.sendOneway(msg, selector, 100); + } + + @Test(expected = org.apache.rocketmq.client.exception.MQClientException.class) + public void testSelectorThrowsException() throws Exception { + Message msg = new Message(topic, RandomUtils.getStringByUUID().getBytes()); + DefaultMQProducer producer = ProducerFactory.getRMQProducer(NAMESRV_ADDR); + producer.sendOneway(msg, new MessageQueueSelector() { + @Override + public MessageQueue select(List list, Message message, Object o) { + String str = null; + return list.get(str.length()); + } + }, null); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/producer/oneway/OneWaySendIT.java b/test/src/test/java/org/apache/rocketmq/test/client/producer/oneway/OneWaySendIT.java new file mode 100644 index 0000000..7e5c76a --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/producer/oneway/OneWaySendIT.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.test.client.producer.oneway; + +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.consumer.tag.TagMessageWith1ConsumerIT; +import org.apache.rocketmq.test.client.rmq.RMQAsyncSendProducer; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class OneWaySendIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(TagMessageWith1ConsumerIT.class); + private RMQAsyncSendProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("user topic[%s]!", topic)); + producer = getAsyncProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testOneWaySendWithOnlyMsgAsParam() { + int msgSize = 20; + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + + producer.sendOneWay(msgSize); + producer.waitForResponse(5 * 1000); + assertThat(producer.getAllMsgBody().size()).isEqualTo(msgSize); + + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/producer/oneway/OneWaySendWithMQIT.java b/test/src/test/java/org/apache/rocketmq/test/client/producer/oneway/OneWaySendWithMQIT.java new file mode 100644 index 0000000..d2699c0 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/producer/oneway/OneWaySendWithMQIT.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.test.client.producer.oneway; + +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.test.base.BaseConf; +import org.apache.rocketmq.test.client.consumer.tag.TagMessageWith1ConsumerIT; +import org.apache.rocketmq.test.client.rmq.RMQAsyncSendProducer; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class OneWaySendWithMQIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(TagMessageWith1ConsumerIT.class); + private static boolean sendFail = false; + private RMQAsyncSendProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("user topic[%s]!", topic)); + producer = getAsyncProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testAsyncSendWithMQ() { + int msgSize = 20; + int queueId = 0; + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + MessageQueue mq = new MessageQueue(topic, BROKER1_NAME, queueId); + + producer.sendOneWay(msgSize, mq); + producer.waitForResponse(5 * 1000); + + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + + + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/producer/oneway/OneWaySendWithSelectorIT.java b/test/src/test/java/org/apache/rocketmq/test/client/producer/oneway/OneWaySendWithSelectorIT.java new file mode 100644 index 0000000..6cad45c --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/producer/oneway/OneWaySendWithSelectorIT.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.test.client.producer.oneway; + +import java.util.List; +import org.apache.rocketmq.client.producer.MessageQueueSelector; +import org.apache.rocketmq.common.message.Message; +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.test.base.BaseConf; +import org.apache.rocketmq.test.client.consumer.tag.TagMessageWith1ConsumerIT; +import org.apache.rocketmq.test.client.rmq.RMQAsyncSendProducer; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class OneWaySendWithSelectorIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(TagMessageWith1ConsumerIT.class); + private static boolean sendFail = false; + private RMQAsyncSendProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("user topic[%s]!", topic)); + producer = getAsyncProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testSendWithSelector() { + int msgSize = 20; + final int queueId = 0; + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + + producer.sendOneWay(msgSize, new MessageQueueSelector() { + @Override + public MessageQueue select(List list, Message message, Object o) { + for (MessageQueue mq : list) { + if (mq.getQueueId() == queueId && mq.getBrokerName().equals(BROKER1_NAME)) { + return mq; + } + } + return list.get(0); + } + }); + assertThat(producer.getAllMsgBody().size()).isEqualTo(msgSize); + + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + + VerifyUtils.verifyMessageQueueId(queueId, consumer.getListener().getAllOriginMsg()); + + producer.clearMsg(); + consumer.clearMsg(); + + producer.sendOneWay(msgSize, new MessageQueueSelector() { + @Override + public MessageQueue select(List list, Message message, Object o) { + for (MessageQueue mq : list) { + if (mq.getQueueId() == queueId && mq.getBrokerName().equals(BROKER2_NAME)) { + return mq; + } + } + return list.get(8); + } + }); + assertThat(producer.getAllMsgBody().size()).isEqualTo(msgSize); + + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + + VerifyUtils.verifyMessageQueueId(queueId, consumer.getListener().getAllOriginMsg()); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/producer/order/OrderMsgDynamicRebalanceIT.java b/test/src/test/java/org/apache/rocketmq/test/client/producer/order/OrderMsgDynamicRebalanceIT.java new file mode 100644 index 0000000..a683d4f --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/producer/order/OrderMsgDynamicRebalanceIT.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.test.client.producer.order; + +import java.util.List; +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.test.base.BaseConf; +import org.apache.rocketmq.test.client.consumer.balance.NormalMsgStaticBalanceIT; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.order.RMQOrderListener; +import org.apache.rocketmq.test.message.MessageQueueMsg; +import org.apache.rocketmq.test.util.MQWait; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class OrderMsgDynamicRebalanceIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(NormalMsgStaticBalanceIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("use topic: %s !", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testTwoConsumerAndCrashOne() { + int msgSize = 10; + RMQNormalConsumer consumer1 = getConsumer(NAMESRV_ADDR, topic, "*", + new RMQOrderListener("1")); + RMQNormalConsumer consumer2 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), topic, + "*", new RMQOrderListener("2")); + + List mqs = producer.getMessageQueue(); + MessageQueueMsg mqMsgs = new MessageQueueMsg(mqs, msgSize); + producer.send(mqMsgs.getMsgsWithMQ()); + + MQWait.waitConsumeAll(30 * 1000, producer.getAllMsgBody(), consumer1.getListener(), + consumer2.getListener()); + consumer2.shutdown(); + + mqMsgs = new MessageQueueMsg(mqs, msgSize); + producer.send(mqMsgs.getMsgsWithMQ()); + + boolean recvAll = MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), + consumer1.getListener(), consumer2.getListener()); + assertThat(recvAll).isEqualTo(true); + + assertThat(VerifyUtils.verifyOrder(((RMQOrderListener) consumer1.getListener()).getMsgs())) + .isEqualTo(true); + assertThat(VerifyUtils.verifyOrder(((RMQOrderListener) consumer2.getListener()).getMsgs())) + .isEqualTo(true); + } + + @Test + public void testThreeConsumerAndCrashOne() { + int msgSize = 10; + RMQNormalConsumer consumer1 = getConsumer(NAMESRV_ADDR, topic, "*", + new RMQOrderListener("1")); + RMQNormalConsumer consumer2 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), topic, + "*", new RMQOrderListener("2")); + RMQNormalConsumer consumer3 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), topic, + "*", new RMQOrderListener("3")); + + List mqs = producer.getMessageQueue(); + MessageQueueMsg mqMsgs = new MessageQueueMsg(mqs, msgSize); + producer.send(mqMsgs.getMsgsWithMQ()); + + MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), consumer1.getListener(), + consumer2.getListener(), consumer3.getListener()); + consumer3.shutdown(); + + mqMsgs = new MessageQueueMsg(mqs, msgSize); + producer.send(mqMsgs.getMsgsWithMQ()); + + boolean recvAll = MQWait.waitConsumeAll(30 * 1000, producer.getAllMsgBody(), + consumer1.getListener(), consumer2.getListener(), consumer3.getListener()); + assertThat(recvAll).isEqualTo(true); + + assertThat(VerifyUtils.verifyOrder(((RMQOrderListener) consumer1.getListener()).getMsgs())) + .isEqualTo(true); + assertThat(VerifyUtils.verifyOrder(((RMQOrderListener) consumer2.getListener()).getMsgs())) + .isEqualTo(true); + assertThat(VerifyUtils.verifyOrder(((RMQOrderListener) consumer3.getListener()).getMsgs())) + .isEqualTo(true); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/producer/order/OrderMsgIT.java b/test/src/test/java/org/apache/rocketmq/test/client/producer/order/OrderMsgIT.java new file mode 100644 index 0000000..a82f56a --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/producer/order/OrderMsgIT.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.test.client.producer.order; + +import java.util.List; +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.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.factory.MQMessageFactory; +import org.apache.rocketmq.test.listener.rmq.order.RMQOrderListener; +import org.apache.rocketmq.test.message.MessageQueueMsg; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class OrderMsgIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(OrderMsgIT.class); + private RMQNormalProducer producer = null; + private RMQNormalConsumer consumer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("use topic: %s;", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + consumer = getConsumer(NAMESRV_ADDR, topic, "*", new RMQOrderListener()); + } + + @After + public void tearDown() { + shutdown(); + } + + @Test + public void testOrderMsg() { + int msgSize = 10; + List mqs = producer.getMessageQueue(); + MessageQueueMsg mqMsgs = new MessageQueueMsg(mqs, msgSize); + producer.send(mqMsgs.getMsgsWithMQ()); + + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(mqMsgs.getMsgBodys()); + + assertThat(VerifyUtils.verifyOrder(((RMQOrderListener) consumer.getListener()).getMsgs())) + .isEqualTo(true); + } + + @Test + public void testSendOneQueue() { + int msgSize = 20; + List mqs = producer.getMessageQueue(); + MessageQueueMsg mqMsgs = new MessageQueueMsg(MQMessageFactory.getMessageQueues(mqs.get(0)), + msgSize); + producer.send(mqMsgs.getMsgsWithMQ()); + + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(mqMsgs.getMsgBodys()); + + assertThat(VerifyUtils.verifyOrder(((RMQOrderListener) consumer.getListener()).getMsgs())) + .isEqualTo(true); + } + + @Test + public void testSendRandomQueues() { + int msgSize = 10; + List mqs = producer.getMessageQueue(); + MessageQueueMsg mqMsgs = new MessageQueueMsg( + MQMessageFactory.getMessageQueues(mqs.get(0), mqs.get(1), mqs.get(mqs.size() - 1)), + msgSize); + producer.send(mqMsgs.getMsgsWithMQ()); + + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(mqMsgs.getMsgBodys()); + + assertThat(VerifyUtils.verifyOrder(((RMQOrderListener) consumer.getListener()).getMsgs())) + .isEqualTo(true); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/producer/order/OrderMsgRebalanceIT.java b/test/src/test/java/org/apache/rocketmq/test/client/producer/order/OrderMsgRebalanceIT.java new file mode 100644 index 0000000..5e3238b --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/producer/order/OrderMsgRebalanceIT.java @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.test.client.producer.order; + +import java.util.List; +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.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.order.RMQOrderListener; +import org.apache.rocketmq.test.message.MessageQueueMsg; +import org.apache.rocketmq.test.util.MQWait; +import org.apache.rocketmq.test.util.TestUtils; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class OrderMsgRebalanceIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(OrderMsgRebalanceIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("use topic: %s !", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testTwoConsumersBalance() { + int msgSize = 10; + RMQNormalConsumer consumer1 = getConsumer(NAMESRV_ADDR, topic, "*", new RMQOrderListener()); + RMQNormalConsumer consumer2 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), topic, + "*", new RMQOrderListener()); + TestUtils.waitForSeconds(WAIT_TIME); + + List mqs = producer.getMessageQueue(); + MessageQueueMsg mqMsgs = new MessageQueueMsg(mqs, msgSize); + producer.send(mqMsgs.getMsgsWithMQ()); + + boolean recvAll = MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), + consumer1.getListener(), consumer2.getListener()); + assertThat(recvAll).isEqualTo(true); + + boolean balance = VerifyUtils.verifyBalance(producer.getAllMsgBody().size(), + VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer1.getListener().getAllUndupMsgBody()).size(), + VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer2.getListener().getAllUndupMsgBody()).size()); + assertThat(balance).isEqualTo(true); + + assertThat(VerifyUtils.verifyOrder(((RMQOrderListener) consumer1.getListener()).getMsgs())) + .isEqualTo(true); + assertThat(VerifyUtils.verifyOrder(((RMQOrderListener) consumer2.getListener()).getMsgs())) + .isEqualTo(true); + } + + @Test + public void testFourConsumerBalance() { + int msgSize = 20; + RMQNormalConsumer consumer1 = getConsumer(NAMESRV_ADDR, topic, "*", new RMQOrderListener()); + RMQNormalConsumer consumer2 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), topic, + "*", new RMQOrderListener()); + RMQNormalConsumer consumer3 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), topic, + "*", new RMQOrderListener()); + RMQNormalConsumer consumer4 = getConsumer(NAMESRV_ADDR, consumer1.getConsumerGroup(), topic, + "*", new RMQOrderListener()); + TestUtils.waitForSeconds(WAIT_TIME); + + List mqs = producer.getMessageQueue(); + MessageQueueMsg mqMsgs = new MessageQueueMsg(mqs, msgSize); + producer.send(mqMsgs.getMsgsWithMQ()); + + boolean recvAll = MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), + consumer1.getListener(), consumer2.getListener(), consumer3.getListener(), + consumer4.getListener()); + assertThat(recvAll).isEqualTo(true); + + boolean balance = VerifyUtils + .verifyBalance(producer.getAllMsgBody().size(), + VerifyUtils + .getFilterdMessage(producer.getAllMsgBody(), + consumer1.getListener().getAllUndupMsgBody()) + .size(), + VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer2.getListener().getAllUndupMsgBody()).size(), + VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer3.getListener().getAllUndupMsgBody()).size(), + VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer4.getListener().getAllUndupMsgBody()).size()); + logger.info(String.format("consumer1:%s;consumer2:%s;consumer3:%s,consumer4:%s", + consumer1.getListener().getAllMsgBody().size(), + consumer2.getListener().getAllMsgBody().size(), + consumer3.getListener().getAllMsgBody().size(), + consumer4.getListener().getAllMsgBody().size())); + assertThat(balance).isEqualTo(true); + + assertThat(VerifyUtils.verifyOrder(((RMQOrderListener) consumer1.getListener()).getMsgs())) + .isEqualTo(true); + assertThat(VerifyUtils.verifyOrder(((RMQOrderListener) consumer2.getListener()).getMsgs())) + .isEqualTo(true); + assertThat(VerifyUtils.verifyOrder(((RMQOrderListener) consumer3.getListener()).getMsgs())) + .isEqualTo(true); + assertThat(VerifyUtils.verifyOrder(((RMQOrderListener) consumer4.getListener()).getMsgs())) + .isEqualTo(true); + } + +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/producer/order/OrderMsgWithTagIT.java b/test/src/test/java/org/apache/rocketmq/test/client/producer/order/OrderMsgWithTagIT.java new file mode 100644 index 0000000..e8363f0 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/producer/order/OrderMsgWithTagIT.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.test.client.producer.order; + +import java.util.List; +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.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.order.RMQOrderListener; +import org.apache.rocketmq.test.message.MessageQueueMsg; +import org.apache.rocketmq.test.util.MQWait; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class OrderMsgWithTagIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(OrderMsgIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("use topic: %s;", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + shutdown(); + } + + @Test + public void testOrderMsgWithTagSubAll() { + int msgSize = 10; + String tag = "jueyin_tag"; + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, "*", new RMQOrderListener()); + + List mqs = producer.getMessageQueue(); + MessageQueueMsg mqMsgs = new MessageQueueMsg(mqs, msgSize, tag); + producer.send(mqMsgs.getMsgsWithMQ()); + + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(mqMsgs.getMsgBodys()); + + assertThat(VerifyUtils.verifyOrder(((RMQOrderListener) consumer.getListener()).getMsgs())) + .isEqualTo(true); + } + + @Test + public void testOrderMsgWithTagSubTag() { + int msgSize = 5; + String tag = "jueyin_tag"; + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, tag, new RMQOrderListener()); + + List mqs = producer.getMessageQueue(); + MessageQueueMsg mqMsgs = new MessageQueueMsg(mqs, msgSize, tag); + producer.send(mqMsgs.getMsgsWithMQ()); + + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(mqMsgs.getMsgBodys()); + + assertThat(VerifyUtils.verifyOrder(((RMQOrderListener) consumer.getListener()).getMsgs())) + .isEqualTo(true); + } + + @Test + public void testOrderMsgWithTag1AndTag2SubTag1() { + int msgSize = 5; + String tag1 = "jueyin_tag_1"; + String tag2 = "jueyin_tag_2"; + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, tag1, new RMQOrderListener()); + + List mqs = producer.getMessageQueue(); + + MessageQueueMsg mqMsgs = new MessageQueueMsg(mqs, msgSize, tag2); + producer.send(mqMsgs.getMsgsWithMQ()); + producer.clearMsg(); + + mqMsgs = new MessageQueueMsg(mqs, msgSize, tag1); + producer.send(mqMsgs.getMsgsWithMQ()); + + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(mqMsgs.getMsgBodys()); + + assertThat(VerifyUtils.verifyOrder(((RMQOrderListener) consumer.getListener()).getMsgs())) + .isEqualTo(true); + } + + @Test + public void testTwoConsumerSubTag() { + int msgSize = 10; + String tag1 = "jueyin_tag_1"; + String tag2 = "jueyin_tag_2"; + RMQNormalConsumer consumer1 = getConsumer(NAMESRV_ADDR, topic, tag1, + new RMQOrderListener("consumer1")); + RMQNormalConsumer consumer2 = getConsumer(NAMESRV_ADDR, topic, tag2, + new RMQOrderListener("consumer2")); + List mqs = producer.getMessageQueue(); + + MessageQueueMsg mqMsgs = new MessageQueueMsg(mqs, msgSize, tag1); + producer.send(mqMsgs.getMsgsWithMQ()); + + mqMsgs = new MessageQueueMsg(mqs, msgSize, tag2); + producer.send(mqMsgs.getMsgsWithMQ()); + + boolean recvAll = MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), + consumer1.getListener(), consumer2.getListener()); + assertThat(recvAll).isEqualTo(true); + + assertThat(VerifyUtils.verifyOrder(((RMQOrderListener) consumer1.getListener()).getMsgs())) + .isEqualTo(true); + assertThat(VerifyUtils.verifyOrder(((RMQOrderListener) consumer2.getListener()).getMsgs())) + .isEqualTo(true); + } + + @Test + public void testConsumeTwoTag() { + int msgSize = 10; + String tag1 = "jueyin_tag_1"; + String tag2 = "jueyin_tag_2"; + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, + String.format("%s||%s", tag1, tag2), new RMQOrderListener()); + + List mqs = producer.getMessageQueue(); + + MessageQueueMsg mqMsgs = new MessageQueueMsg(mqs, msgSize, tag1); + producer.send(mqMsgs.getMsgsWithMQ()); + + mqMsgs = new MessageQueueMsg(mqs, msgSize, tag2); + producer.send(mqMsgs.getMsgsWithMQ()); + + boolean recvAll = MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), + consumer.getListener()); + assertThat(recvAll).isEqualTo(true); + + assertThat(VerifyUtils.verifyOrder(((RMQOrderListener) consumer.getListener()).getMsgs())) + .isEqualTo(true); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/producer/querymsg/QueryMsgByIdExceptionIT.java b/test/src/test/java/org/apache/rocketmq/test/client/producer/querymsg/QueryMsgByIdExceptionIT.java new file mode 100644 index 0000000..69cddbc --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/producer/querymsg/QueryMsgByIdExceptionIT.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.test.client.producer.querymsg; + +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.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class QueryMsgByIdExceptionIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(QueryMsgByKeyIT.class); + private static RMQNormalProducer producer = null; + private static String topic = null; + + @BeforeClass + public static void setUp() { + topic = initTopic(); + logger.info(String.format("use topic: %s;", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @AfterClass + public static void tearDown() { + shutdown(); + } + + @Test + public void testQueryMsgByErrorMsgId() { + producer.clearMsg(); + int msgSize = 20; + String errorMsgId = "errorMsgId"; + producer.send(msgSize); + Assert.assertEquals("Not all are sent", msgSize, producer.getAllUndupMsgBody().size()); + + MessageExt queryMsg = null; + try { + queryMsg = producer.getProducer().viewMessage("topic", errorMsgId); + } catch (Exception e) { + } + + assertThat(queryMsg).isNull(); + } + + @Test + public void testQueryMsgByNullMsgId() { + producer.clearMsg(); + int msgSize = 20; + String errorMsgId = null; + producer.send(msgSize); + Assert.assertEquals("Not all are sent", msgSize, producer.getAllUndupMsgBody().size()); + + MessageExt queryMsg = null; + try { + queryMsg = producer.getProducer().viewMessage("topic", errorMsgId); + } catch (Exception e) { + } + + assertThat(queryMsg).isNull(); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/producer/querymsg/QueryMsgByIdIT.java b/test/src/test/java/org/apache/rocketmq/test/client/producer/querymsg/QueryMsgByIdIT.java new file mode 100644 index 0000000..b14b399 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/producer/querymsg/QueryMsgByIdIT.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.test.client.producer.querymsg; + +import org.apache.rocketmq.common.message.MessageClientExt; +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.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.TestUtils; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class QueryMsgByIdIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(QueryMsgByIdIT.class); + private RMQNormalProducer producer = null; + private RMQNormalConsumer consumer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("use topic: %s;", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + consumer = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + } + + @After + public void tearDown() { + shutdown(); + } + + @Test + public void testQueryMsg() { + int msgSize = 20; + producer.send(msgSize); + Assert.assertEquals("Not all are sent", msgSize, producer.getAllUndupMsgBody().size()); + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + Assert.assertEquals("Not all are consumed", 0, VerifyUtils.verify(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())); + + MessageExt recvMsg = (MessageExt) consumer.getListener().getFirstMsg(); + MessageExt queryMsg = null; + try { + TestUtils.waitForMoment(3000); + queryMsg = producer.getProducer().viewMessage(recvMsg.getTopic(), ((MessageClientExt) recvMsg).getOffsetMsgId()); + } catch (Exception e) { + } + + assertThat(queryMsg).isNotNull(); + assertThat(new String(queryMsg.getBody())).isEqualTo(new String(recvMsg.getBody())); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/producer/querymsg/QueryMsgByKeyIT.java b/test/src/test/java/org/apache/rocketmq/test/client/producer/querymsg/QueryMsgByKeyIT.java new file mode 100644 index 0000000..69dd26c --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/producer/querymsg/QueryMsgByKeyIT.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.test.client.producer.querymsg; + +import java.util.List; + +import org.apache.rocketmq.client.exception.MQClientException; +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.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.factory.MQMessageFactory; +import org.apache.rocketmq.test.util.TestUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class QueryMsgByKeyIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(QueryMsgByKeyIT.class); + private RMQNormalProducer producer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("use topic: %s;", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + shutdown(); + } + + @Test + public void testQueryMsg() { + int msgSize = 20; + String key = "jueyin"; + long begin = System.currentTimeMillis(); + List msgs = MQMessageFactory.getKeyMsg(topic, key, msgSize); + producer.send(msgs); + Assert.assertEquals("Not all are sent", msgSize, producer.getAllUndupMsgBody().size()); + + List queryMsgs = null; + try { + TestUtils.waitForMoment(500 * 3); + queryMsgs = producer.getProducer().queryMessage(topic, key, msgSize, begin - 5000, + System.currentTimeMillis() + 5000).getMessageList(); + } catch (Exception e) { + } + + assertThat(queryMsgs).isNotNull(); + assertThat(queryMsgs.size()).isEqualTo(msgSize); + } + + @Test + public void testQueryMax() { + int msgSize = 500; + int max = 64 * BROKER_NUM; + String key = "jueyin"; + long begin = System.currentTimeMillis(); + List msgs = MQMessageFactory.getKeyMsg(topic, key, msgSize); + producer.send(msgs); + Assert.assertEquals("Not all are sent", msgSize, producer.getAllUndupMsgBody().size()); + + List queryMsgs = null; + try { + queryMsgs = producer.getProducer().queryMessage(topic, key, msgSize, begin - 15000, + System.currentTimeMillis() + 15000).getMessageList(); + + int i = 3; + while (queryMsgs == null || queryMsgs.size() != BROKER_NUM) { + i--; + queryMsgs = producer.getProducer().queryMessage(topic, key, msgSize, begin - 15000, + System.currentTimeMillis() + 15000).getMessageList(); + TestUtils.waitForMoment(1000); + + if (i == 0 || queryMsgs != null && queryMsgs.size() == max) { + break; + } + } + } catch (Exception e) { + } + + assertThat(queryMsgs).isNotNull(); + assertThat(queryMsgs.size()).isEqualTo(max); + } + + + @Test(expected = MQClientException.class) + public void testQueryMsgWithSameHash1() throws Exception { + int msgSize = 1; + String topicA = "AaTopic"; + String keyA = "Aa"; + String topicB = "BBTopic"; + String keyB = "BB"; + + initTopicWithName(topicA); + initTopicWithName(topicB); + + RMQNormalProducer producerA = getProducer(NAMESRV_ADDR, topicA); + RMQNormalProducer producerB = getProducer(NAMESRV_ADDR, topicB); + + List msgA = MQMessageFactory.getKeyMsg(topicA, keyA, msgSize); + List msgB = MQMessageFactory.getKeyMsg(topicB, keyB, msgSize); + + producerA.send(msgA); + producerB.send(msgB); + + long begin = System.currentTimeMillis() - 500000; + long end = System.currentTimeMillis() + 500000; + producerA.getProducer().queryMessage(topicA, keyB, msgSize * 10, begin, end).getMessageList(); + } + + + @Test + public void testQueryMsgWithSameHash2() throws Exception { + int msgSize = 1; + String topicA = "AaAaTopic"; + String keyA = "Aa"; + String topicB = "BBBBTopic"; + String keyB = "Aa"; + + initTopicWithName(topicA); + initTopicWithName(topicB); + + RMQNormalProducer producerA = getProducer(NAMESRV_ADDR, topicA); + RMQNormalProducer producerB = getProducer(NAMESRV_ADDR, topicB); + + List msgA = MQMessageFactory.getKeyMsg(topicA, keyA, msgSize); + List msgB = MQMessageFactory.getKeyMsg(topicB, keyB, msgSize); + + producerA.send(msgA); + producerB.send(msgB); + + long begin = System.currentTimeMillis() - 500000; + long end = System.currentTimeMillis() + 500000; + List list = producerA.getProducer().queryMessage(topicA, keyA, msgSize * 10, begin, end).getMessageList(); + + assertThat(list).isNotNull(); + assertThat(list.size()).isEqualTo(1); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/client/producer/transaction/TransactionalMsgIT.java b/test/src/test/java/org/apache/rocketmq/test/client/producer/transaction/TransactionalMsgIT.java new file mode 100644 index 0000000..ab1d2c1 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/client/producer/transaction/TransactionalMsgIT.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.test.client.producer.transaction; + +import org.apache.rocketmq.client.producer.LocalTransactionState; +import org.apache.rocketmq.client.producer.TransactionListener; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.message.Message; +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.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQTransactionalProducer; +import org.apache.rocketmq.test.factory.MQMessageFactory; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.MQWait; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import static com.google.common.truth.Truth.assertThat; + +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +public class TransactionalMsgIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(TransactionalMsgIT.class); + private RMQTransactionalProducer producer = null; + private RMQNormalConsumer consumer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("use topic: %s;", topic)); + producer = getTransactionalProducer(NAMESRV_ADDR, topic, new TransactionListenerImpl()); + consumer = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testMessageVisibility() throws Exception { + Thread.sleep(3000); + int msgSize = 120; + List msgs = MQMessageFactory.getMsg(topic, msgSize); + for (int i = 0; i < msgSize; i++) { + producer.send(msgs.get(i), getTransactionHandle(i)); + } + boolean recvAll = MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), consumer.getListener()); + assertThat(recvAll).isEqualTo(true); + } + + static Pair getTransactionHandle(int msgIndex) { + switch (msgIndex % 5) { + case 0: + //commit immediately + return new Pair<>(true, LocalTransactionState.COMMIT_MESSAGE); + case 1: + //rollback immediately + return new Pair<>(true, LocalTransactionState.ROLLBACK_MESSAGE); + case 2: + //commit in check + return new Pair<>(false, LocalTransactionState.COMMIT_MESSAGE); + case 3: + //rollback in check + return new Pair<>(false, LocalTransactionState.ROLLBACK_MESSAGE); + case 4: + default: + return new Pair<>(false, LocalTransactionState.UNKNOW); + + } + } + + static private class TransactionListenerImpl implements TransactionListener { + ConcurrentHashMap checkStatus = new ConcurrentHashMap<>(); + + @Override + public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { + Pair transactionHandle = (Pair) arg; + if (transactionHandle.getObject1()) { + return transactionHandle.getObject2(); + } else { + checkStatus.put(msg.getTransactionId(), transactionHandle.getObject2()); + return LocalTransactionState.UNKNOW; + } + } + + @Override + public LocalTransactionState checkLocalTransaction(MessageExt msg) { + LocalTransactionState state = checkStatus.get(msg.getTransactionId()); + if (state == null) { + return LocalTransactionState.UNKNOW; + } else { + return state; + } + } + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/container/AddAndRemoveBrokerIT.java b/test/src/test/java/org/apache/rocketmq/test/container/AddAndRemoveBrokerIT.java new file mode 100644 index 0000000..7bdfdd7 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/container/AddAndRemoveBrokerIT.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.test.container; + +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.container.BrokerContainer; +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.protocol.ResponseCode; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@Ignore +public class AddAndRemoveBrokerIT extends ContainerIntegrationTestBase { + private static BrokerContainer brokerContainer4; + + @BeforeClass + public static void beforeClass() { + brokerContainer4 = createAndStartBrokerContainer(nsAddr); + } + + @AfterClass + public static void afterClass() { + brokerContainer4.shutdown(); + } + + @Test + public void addBrokerTest() + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, + RemotingConnectException { + String remark = null; + int code = 0; + try { + defaultMQAdminExt.addBrokerToContainer(brokerContainer4.getBrokerContainerAddr(), ""); + } catch (MQBrokerException e) { + code = e.getResponseCode(); + remark = e.getErrorMessage(); + } + assertThat(code).isEqualTo(ResponseCode.SYSTEM_ERROR); + assertThat(remark).isEqualTo("addBroker properties empty"); + } + + @Test + public void removeBrokerTest() + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException { + + boolean exceptionCaught = false; + + try { + defaultMQAdminExt.removeBrokerFromContainer(brokerContainer1.getBrokerContainerAddr(), + master3With3Replicas.getBrokerConfig().getBrokerClusterName(), + master3With3Replicas.getBrokerConfig().getBrokerName(), 1); + } catch (MQBrokerException e) { + exceptionCaught = true; + } + + assertThat(exceptionCaught).isFalse(); + assertThat(brokerContainer1.getSlaveBrokers().size()).isEqualTo(1); + + createAndAddSlave(1, brokerContainer1, master3With3Replicas); + awaitUntilSlaveOK(); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/container/BrokerFailoverIT.java b/test/src/test/java/org/apache/rocketmq/test/container/BrokerFailoverIT.java new file mode 100644 index 0000000..fcf94a0 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/container/BrokerFailoverIT.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.test.container; + +import java.time.Duration; +import org.apache.rocketmq.container.InnerSalveBrokerController; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.junit.Ignore; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@Ignore +public class BrokerFailoverIT extends ContainerIntegrationTestBase { + + @Test + public void testBrokerFailoverWithoutCompatible() { + changeCompatibleMode(false); + awaitUntilSlaveOK(); + testBrokerFailover(false); + } + + @Test + public void testBrokerFailoverWithCompatible() { + changeCompatibleMode(true); + awaitUntilSlaveOK(); + testBrokerFailover(true); + } + + private void testBrokerFailover(boolean compatibleMode) { + await().atMost(Duration.ofSeconds(10)).until(() -> + master1With3Replicas.getMessageStore().getAliveReplicaNumInGroup() == 3 + && master2With3Replicas.getMessageStore().getAliveReplicaNumInGroup() == 3 + && master3With3Replicas.getMessageStore().getAliveReplicaNumInGroup() == 3); + + InnerSalveBrokerController targetSlave = getSlaveFromContainerByName(brokerContainer2, master1With3Replicas.getBrokerConfig().getBrokerName()); + + assertThat(targetSlave).isNotNull(); + + brokerContainer1.registerClientRPCHook(new RPCHook() { + @Override + public void doBeforeRequest(String remoteAddr, RemotingCommand request) { + if (request.getCode() == (compatibleMode ? RequestCode.QUERY_DATA_VERSION : RequestCode.BROKER_HEARTBEAT)) { + request.setCode(-1); + } + } + + @Override + public void doAfterResponse(String remoteAddr, RemotingCommand request, + RemotingCommand response) { + + } + }); + + InnerSalveBrokerController finalTargetSlave = targetSlave; + await().atMost(Duration.ofSeconds(60)).until(() -> + finalTargetSlave.getMessageStore().getAliveReplicaNumInGroup() == 2 + && master2With3Replicas.getMessageStore().getAliveReplicaNumInGroup() == 2 + && master3With3Replicas.getMessageStore().getAliveReplicaNumInGroup() == 2); + + brokerContainer1.clearClientRPCHook(); + + await().atMost(Duration.ofSeconds(60)).until(() -> + master1With3Replicas.getMessageStore().getAliveReplicaNumInGroup() == 3 + && master2With3Replicas.getMessageStore().getAliveReplicaNumInGroup() == 3 + && master3With3Replicas.getMessageStore().getAliveReplicaNumInGroup() == 3); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/container/BrokerMemberGroupIT.java b/test/src/test/java/org/apache/rocketmq/test/container/BrokerMemberGroupIT.java new file mode 100644 index 0000000..303af38 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/container/BrokerMemberGroupIT.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.test.container; + +import java.time.Duration; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.remoting.protocol.body.BrokerMemberGroup; +import org.junit.Ignore; +import org.junit.Test; + +import static org.awaitility.Awaitility.await; + +@Ignore +public class BrokerMemberGroupIT extends ContainerIntegrationTestBase { + @Test + public void testSyncBrokerMemberGroup() throws Exception { + await().atMost(Duration.ofSeconds(5)).until(() -> { + final BrokerConfig brokerConfig = master1With3Replicas.getBrokerConfig(); + final BrokerMemberGroup memberGroup = master1With3Replicas.getBrokerOuterAPI() + .syncBrokerMemberGroup(brokerConfig.getBrokerClusterName(), brokerConfig.getBrokerName()); + + return memberGroup.getBrokerAddrs().size() == 3; + }); + + await().atMost(Duration.ofSeconds(5)).until(() -> { + final BrokerConfig brokerConfig = master3With3Replicas.getBrokerConfig(); + final BrokerMemberGroup memberGroup = master3With3Replicas.getBrokerOuterAPI() + .syncBrokerMemberGroup(brokerConfig.getBrokerClusterName(), brokerConfig.getBrokerName()); + + return memberGroup.getBrokerAddrs().size() == 3; + }); + + removeSlaveBroker(1, brokerContainer1, master3With3Replicas); + removeSlaveBroker(1, brokerContainer2, master1With3Replicas); + + await().atMost(Duration.ofSeconds(5)).until(() -> { + final BrokerConfig brokerConfig = master1With3Replicas.getBrokerConfig(); + final BrokerMemberGroup memberGroup = master1With3Replicas.getBrokerOuterAPI() + .syncBrokerMemberGroup(brokerConfig.getBrokerClusterName(), brokerConfig.getBrokerName()); + + return memberGroup.getBrokerAddrs().size() == 2 && memberGroup.getBrokerAddrs().get(1L) == null; + }); + + await().atMost(Duration.ofSeconds(5)).until(() -> { + final BrokerConfig brokerConfig = master3With3Replicas.getBrokerConfig(); + final BrokerMemberGroup memberGroup = master3With3Replicas.getBrokerOuterAPI() + .syncBrokerMemberGroup(brokerConfig.getBrokerClusterName(), brokerConfig.getBrokerName()); + return memberGroup.getBrokerAddrs().size() == 2 && memberGroup.getBrokerAddrs().get(1L) == null; + }); + + createAndAddSlave(1, brokerContainer2, master1With3Replicas); + createAndAddSlave(1, brokerContainer1, master3With3Replicas); + + awaitUntilSlaveOK(); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/container/ContainerIntegrationTestBase.java b/test/src/test/java/org/apache/rocketmq/test/container/ContainerIntegrationTestBase.java new file mode 100644 index 0000000..016f908 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/container/ContainerIntegrationTestBase.java @@ -0,0 +1,662 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.test.container; + +import io.netty.channel.ChannelHandlerContext; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +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.BrokerConfig; +import org.apache.rocketmq.common.BrokerIdentity; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.namesrv.NamesrvConfig; +import org.apache.rocketmq.container.BrokerContainer; +import org.apache.rocketmq.container.BrokerContainerConfig; +import org.apache.rocketmq.container.InnerSalveBrokerController; +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.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; +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.header.namesrv.RegisterBrokerRequestHeader; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.ha.HAConnection; +import org.apache.rocketmq.store.ha.HAConnectionState; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.junit.Assert; +import org.junit.BeforeClass; + +import static org.awaitility.Awaitility.await; + +/** + * ContainerIntegrationTestBase will setup a rocketmq ha cluster contains two broker group: + *
  • BrokerA contains two replicas
  • + *
  • BrokerB contains three replicas
  • + */ +public class ContainerIntegrationTestBase { + private static final AtomicBoolean CLUSTER_SET_UP = new AtomicBoolean(false); + private static final List TMP_FILE_LIST = new ArrayList<>(); + private static final Random RANDOM = new Random(); + protected static String nsAddr; + + protected static final String THREE_REPLICAS_TOPIC = "SEND_MESSAGE_TEST_TOPIC_THREE_REPLICAS"; + + protected static List brokerContainerList = new ArrayList<>(); + protected static List namesrvControllers = new ArrayList<>(); + + protected static final String BROKER_NAME_PREFIX = "TestBrokerName_"; + protected static final int COMMIT_LOG_SIZE = 128 * 1024; + protected static final int INDEX_NUM = 1000; + protected static final AtomicInteger BROKER_INDEX = new AtomicInteger(0); + + protected static BrokerContainer brokerContainer1; + protected static BrokerContainer brokerContainer2; + protected static BrokerContainer brokerContainer3; + protected static BrokerController master1With3Replicas; + protected static BrokerController master2With3Replicas; + protected static BrokerController master3With3Replicas; + protected static NamesrvController namesrvController; + + protected static DefaultMQAdminExt defaultMQAdminExt; + + private final static Logger LOG = LoggerFactory.getLogger(ContainerIntegrationTestBase.class); + private static ConcurrentMap slaveStoreConfigCache = new ConcurrentHashMap<>(); + + protected static ConcurrentMap isolatedBrokers = new ConcurrentHashMap<>(); + private static final Set PORTS_IN_USE = new HashSet<>(); + + @BeforeClass + public static void setUp() throws Exception { + if (CLUSTER_SET_UP.compareAndSet(false, true)) { + System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION)); + System.setProperty("rocketmq.broker.diskSpaceCleanForciblyRatio", "0.99"); + System.setProperty("rocketmq.broker.diskSpaceWarningLevelRatio", "0.99"); + + setUpCluster(); + setUpTopic(); + registerCleaner(); + + System.out.printf("cluster setup complete%n"); + } + } + + private static void setUpTopic() { + createTopic(THREE_REPLICAS_TOPIC); + } + + private static void createTopic(String topic) { + createTopicTo(master1With3Replicas, topic); + createTopicTo(master2With3Replicas, topic); + createTopicTo(master3With3Replicas, topic); + } + + private static void setUpCluster() throws Exception { + namesrvController = createAndStartNamesrv(); + nsAddr = "127.0.0.1:" + namesrvController.getNettyServerConfig().getListenPort(); + System.out.printf("namesrv addr: %s%n", nsAddr); + + /* + * BrokerContainer1 | BrokerContainer2 | BrokerContainer3 + * + * master1With3Replicas(m) master2With3Replicas(m) master3With3Replicas(m) + * master3With3Replicas(s0) master1With3Replicas(s0) master2With3Replicas(s0) + * master2With3Replicas(s1) master3With3Replicas(s1) master1With3Replicas(s1) + */ + + brokerContainer1 = createAndStartBrokerContainer(nsAddr); + brokerContainer2 = createAndStartBrokerContainer(nsAddr); + brokerContainer3 = createAndStartBrokerContainer(nsAddr); + // Create three broker groups, two contains two replicas, another contains three replicas + master1With3Replicas = createAndAddMaster(brokerContainer1, new BrokerGroupConfig(), BROKER_INDEX.getAndIncrement()); + master2With3Replicas = createAndAddMaster(brokerContainer2, new BrokerGroupConfig(), BROKER_INDEX.getAndIncrement()); + master3With3Replicas = createAndAddMaster(brokerContainer3, new BrokerGroupConfig(), BROKER_INDEX.getAndIncrement()); + + createAndAddSlave(1, brokerContainer1, master3With3Replicas); + createAndAddSlave(1, brokerContainer2, master1With3Replicas); + createAndAddSlave(1, brokerContainer3, master2With3Replicas); + + createAndAddSlave(2, brokerContainer1, master2With3Replicas); + createAndAddSlave(2, brokerContainer2, master3With3Replicas); + createAndAddSlave(2, brokerContainer3, master1With3Replicas); + + awaitUntilSlaveOK(); + + defaultMQAdminExt = new DefaultMQAdminExt("HATest_Admin_Group"); + defaultMQAdminExt.setNamesrvAddr(nsAddr); + defaultMQAdminExt.start(); + } + + protected static void createTopicTo(BrokerController masterBroker, String topicName, int rqn, int wqn) { + try { + TopicConfig topicConfig = new TopicConfig(topicName, rqn, wqn, 6, 0); + defaultMQAdminExt.createAndUpdateTopicConfig(masterBroker.getBrokerAddr(), topicConfig); + triggerSlaveSync(masterBroker.getBrokerConfig().getBrokerName(), brokerContainer1); + triggerSlaveSync(masterBroker.getBrokerConfig().getBrokerName(), brokerContainer2); + triggerSlaveSync(masterBroker.getBrokerConfig().getBrokerName(), brokerContainer3); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Create topic to broker failed", e); + } + } + + protected static void createGroup(BrokerController masterBroker, String groupName) { + try { + SubscriptionGroupConfig config = new SubscriptionGroupConfig(); + config.setGroupName(groupName); + + masterBroker.getSubscriptionGroupManager().updateSubscriptionGroupConfig(config); + + triggerSlaveSync(masterBroker.getBrokerConfig().getBrokerName(), brokerContainer1); + triggerSlaveSync(masterBroker.getBrokerConfig().getBrokerName(), brokerContainer2); + triggerSlaveSync(masterBroker.getBrokerConfig().getBrokerName(), brokerContainer3); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Create group to broker failed", e); + } + } + + private static void triggerSlaveSync(String brokerName, BrokerContainer brokerContainer) { + for (InnerSalveBrokerController slaveBroker : brokerContainer.getSlaveBrokers()) { + if (slaveBroker.getBrokerConfig().getBrokerName().equals(brokerName)) { + slaveBroker.getSlaveSynchronize().syncAll(); + slaveBroker.registerBrokerAll(true, false, true); + } + } + } + + protected static void createTopicTo(BrokerController brokerController, String topicName) { + createTopicTo(brokerController, topicName, 8, 8); + } + + private static void registerCleaner() { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + if (CLUSTER_SET_UP.compareAndSet(true, false)) { + System.out.printf("clean up%n"); + defaultMQAdminExt.shutdown(); + + for (final BrokerContainer brokerContainer : brokerContainerList) { + brokerContainer.shutdown(); + for (BrokerController brokerController : brokerContainer.getBrokerControllers()) { + brokerController.getMessageStore().destroy(); + } + } + + for (final NamesrvController namesrvController : namesrvControllers) { + namesrvController.shutdown(); + } + + for (final File file : TMP_FILE_LIST) { + UtilAll.deleteFile(file); + } + } + })); + } + + 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); + } + } + + public static NamesrvController createAndStartNamesrv() { + String baseDir = createBaseDir("test-cluster-namesrv").getAbsolutePath(); + NamesrvConfig namesrvConfig = new NamesrvConfig(); + NettyServerConfig nameServerNettyServerConfig = new NettyServerConfig(); + namesrvConfig.setKvConfigPath(baseDir + File.separator + "namesrv" + File.separator + "kvConfig.json"); + namesrvConfig.setConfigStorePath(baseDir + File.separator + "namesrv" + File.separator + "namesrv.properties"); + namesrvConfig.setSupportActingMaster(true); + namesrvConfig.setScanNotActiveBrokerInterval(1000); + + nameServerNettyServerConfig.setListenPort(generatePort(10000, 10000)); + NamesrvController namesrvController = new NamesrvController(namesrvConfig, nameServerNettyServerConfig); + try { + Assert.assertTrue(namesrvController.initialize()); + LOG.info("Name Server Start:{}", nameServerNettyServerConfig.getListenPort()); + namesrvController.start(); + } catch (Exception e) { + LOG.info("Name Server start failed"); + e.printStackTrace(); + System.exit(1); + } + + namesrvController.getRemotingServer().registerProcessor(RequestCode.REGISTER_BROKER, new NettyRequestProcessor() { + @Override + public RemotingCommand processRequest(final ChannelHandlerContext ctx, + final RemotingCommand request) throws Exception { + final RegisterBrokerRequestHeader requestHeader = (RegisterBrokerRequestHeader) request.decodeCommandCustomHeader(RegisterBrokerRequestHeader.class); + final BrokerConfigLite liteConfig = new BrokerConfigLite(requestHeader.getClusterName(), + requestHeader.getBrokerName(), + requestHeader.getBrokerAddr(), + requestHeader.getBrokerId()); + if (isolatedBrokers.containsKey(liteConfig)) { + // return response with SYSTEM_ERROR + return RemotingCommand.createResponseCommand(null); + } + return namesrvController.getRemotingServer().getDefaultProcessorPair().getObject1().processRequest(ctx, request); + } + + @Override + public boolean rejectRequest() { + return false; + } + }, null); + + namesrvControllers.add(namesrvController); + return namesrvController; + + } + + public static BrokerContainer createAndStartBrokerContainer(String nsAddr) { + BrokerContainerConfig brokerContainerConfig = new BrokerContainerConfig(); + NettyServerConfig nettyServerConfig = new NettyServerConfig(); + NettyClientConfig nettyClientConfig = new NettyClientConfig(); + brokerContainerConfig.setNamesrvAddr(nsAddr); + + nettyServerConfig.setListenPort(generatePort(20000, 10000)); + BrokerContainer brokerContainer = new BrokerContainer(brokerContainerConfig, nettyServerConfig, nettyClientConfig); + try { + Assert.assertTrue(brokerContainer.initialize()); + LOG.info("Broker container Start, listen on {}.", nettyServerConfig.getListenPort()); + brokerContainer.start(); + } catch (Exception e) { + LOG.info("Broker container start failed", e); + e.printStackTrace(); + System.exit(1); + } + brokerContainerList.add(brokerContainer); + return brokerContainer; + } + + private 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; + } + + public static BrokerController createAndAddMaster(BrokerContainer brokerContainer, + BrokerGroupConfig brokerGroupConfig, int brokerIndex) throws Exception { + BrokerConfig brokerConfig = new BrokerConfig(); + MessageStoreConfig storeConfig = new MessageStoreConfig(); + brokerConfig.setBrokerName(BROKER_NAME_PREFIX + brokerIndex); + brokerConfig.setBrokerIP1("127.0.0.1"); + brokerConfig.setBrokerIP2("127.0.0.1"); + brokerConfig.setBrokerId(0); + brokerConfig.setEnablePropertyFilter(true); + brokerConfig.setEnableSlaveActingMaster(brokerGroupConfig.enableSlaveActingMaster); + brokerConfig.setEnableRemoteEscape(brokerGroupConfig.enableRemoteEscape); + brokerConfig.setSlaveReadEnable(brokerGroupConfig.slaveReadEnable); + brokerConfig.setLockInStrictMode(true); + brokerConfig.setConsumerOffsetUpdateVersionStep(10); + brokerConfig.setDelayOffsetUpdateVersionStep(10); + brokerConfig.setCompatibleWithOldNameSrv(false); + brokerConfig.setListenPort(generatePort(brokerContainer.getRemotingServer().localListenPort(), 10000)); + + String baseDir = createBaseDir(brokerConfig.getBrokerName() + "_" + brokerConfig.getBrokerId()).getAbsolutePath(); + storeConfig.setStorePathRootDir(baseDir); + storeConfig.setStorePathCommitLog(baseDir + File.separator + "commitlog"); + storeConfig.setHaListenPort(generatePort(30000, 10000)); + storeConfig.setMappedFileSizeCommitLog(COMMIT_LOG_SIZE); + storeConfig.setMaxIndexNum(INDEX_NUM); + storeConfig.setMaxHashSlotNum(INDEX_NUM * 4); + storeConfig.setTotalReplicas(brokerGroupConfig.totalReplicas); + storeConfig.setInSyncReplicas(brokerGroupConfig.inSyncReplicas); + storeConfig.setMinInSyncReplicas(brokerGroupConfig.minReplicas); + storeConfig.setEnableAutoInSyncReplicas(brokerGroupConfig.autoReplicas); + storeConfig.setBrokerRole(BrokerRole.SYNC_MASTER); + storeConfig.setSyncFlushTimeout(10 * 1000); + + System.out.printf("start master %s with port %d-%d%n", brokerConfig.getCanonicalName(), brokerConfig.getListenPort(), storeConfig.getHaListenPort()); + BrokerController brokerController = null; + try { + brokerController = brokerContainer.addBroker(brokerConfig, storeConfig); + Assert.assertNotNull(brokerController); + brokerController.start(); + TMP_FILE_LIST.add(new File(brokerController.getTopicConfigManager().configFilePath())); + TMP_FILE_LIST.add(new File(brokerController.getSubscriptionGroupManager().configFilePath())); + LOG.info("Broker Start name:{} addr:{}", brokerConfig.getBrokerName(), brokerController.getBrokerAddr()); + } catch (Exception e) { + LOG.info("Broker start failed", e); + e.printStackTrace(); + System.exit(1); + } + + return brokerController; + } + + protected static DefaultMQProducer createProducer(String producerGroup) { + DefaultMQProducer producer = new DefaultMQProducer(producerGroup); + producer.setInstanceName(UUID.randomUUID().toString()); + producer.setNamesrvAddr(nsAddr); + return producer; + } + + protected static TransactionMQProducer createTransactionProducer(String producerGroup, + TransactionCheckListener transactionCheckListener) { + TransactionMQProducer producer = new TransactionMQProducer(producerGroup); + producer.setInstanceName(UUID.randomUUID().toString()); + producer.setNamesrvAddr(nsAddr); + producer.setTransactionCheckListener(transactionCheckListener); + return producer; + } + + protected static TransactionMQProducer createTransactionProducer(String producerGroup, + TransactionListener transactionListener) { + TransactionMQProducer producer = new TransactionMQProducer(producerGroup); + producer.setInstanceName(UUID.randomUUID().toString()); + producer.setNamesrvAddr(nsAddr); + producer.setTransactionListener(transactionListener); + return producer; + } + + protected static DefaultMQPullConsumer createPullConsumer(String consumerGroup) { + DefaultMQPullConsumer consumer = new DefaultMQPullConsumer(consumerGroup); + consumer.setInstanceName(UUID.randomUUID().toString()); + consumer.setNamesrvAddr(nsAddr); + return consumer; + } + + protected static DefaultMQPushConsumer createPushConsumer(String consumerGroup) { + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(consumerGroup); + consumer.setInstanceName(UUID.randomUUID().toString()); + consumer.setNamesrvAddr(nsAddr); + return consumer; + } + + protected static void createAndAddSlave(int slaveBrokerId, BrokerContainer brokerContainer, + BrokerController master) { + BrokerConfig slaveBrokerConfig = new BrokerConfig(); + slaveBrokerConfig.setBrokerName(master.getBrokerConfig().getBrokerName()); + slaveBrokerConfig.setBrokerId(slaveBrokerId); + slaveBrokerConfig.setBrokerClusterName(master.getBrokerConfig().getBrokerClusterName()); + slaveBrokerConfig.setCompatibleWithOldNameSrv(false); + slaveBrokerConfig.setBrokerIP1("127.0.0.1"); + slaveBrokerConfig.setBrokerIP2("127.0.0.1"); + slaveBrokerConfig.setEnablePropertyFilter(true); + slaveBrokerConfig.setSlaveReadEnable(true); + slaveBrokerConfig.setEnableSlaveActingMaster(true); + slaveBrokerConfig.setEnableRemoteEscape(true); + slaveBrokerConfig.setLockInStrictMode(true); + slaveBrokerConfig.setListenPort(generatePort(brokerContainer.getRemotingServer().localListenPort(), 10000)); + slaveBrokerConfig.setConsumerOffsetUpdateVersionStep(10); + slaveBrokerConfig.setDelayOffsetUpdateVersionStep(10); + + MessageStoreConfig storeConfig = slaveStoreConfigCache.get(slaveBrokerConfig); + + if (storeConfig == null) { + storeConfig = new MessageStoreConfig(); + String baseDir = createBaseDir(slaveBrokerConfig.getBrokerName() + "_" + slaveBrokerConfig.getBrokerId()).getAbsolutePath(); + storeConfig.setStorePathRootDir(baseDir); + storeConfig.setStorePathCommitLog(baseDir + File.separator + "commitlog"); + storeConfig.setHaListenPort(generatePort(master.getMessageStoreConfig().getHaListenPort(), 10000)); + storeConfig.setMappedFileSizeCommitLog(COMMIT_LOG_SIZE); + storeConfig.setMaxIndexNum(INDEX_NUM); + storeConfig.setMaxHashSlotNum(INDEX_NUM * 4); + storeConfig.setTotalReplicas(master.getMessageStoreConfig().getTotalReplicas()); + storeConfig.setInSyncReplicas(master.getMessageStoreConfig().getInSyncReplicas()); + storeConfig.setMinInSyncReplicas(master.getMessageStoreConfig().getMinInSyncReplicas()); + storeConfig.setBrokerRole(BrokerRole.SLAVE); + slaveStoreConfigCache.put(slaveBrokerConfig, storeConfig); + } + + System.out.printf("start slave %s with port %d-%d%n", slaveBrokerConfig.getCanonicalName(), slaveBrokerConfig.getListenPort(), storeConfig.getHaListenPort()); + + try { + BrokerController brokerController = brokerContainer.addBroker(slaveBrokerConfig, storeConfig); + Assert.assertNotNull(brokerContainer); + brokerController.start(); + TMP_FILE_LIST.add(new File(brokerController.getTopicConfigManager().configFilePath())); + TMP_FILE_LIST.add(new File(brokerController.getSubscriptionGroupManager().configFilePath())); + LOG.info("Add slave name:{} addr:{}", slaveBrokerConfig.getBrokerName(), brokerController.getBrokerAddr()); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Couldn't add slave broker", e); + } + } + + protected static void removeSlaveBroker(int slaveBrokerId, BrokerContainer brokerContainer, + BrokerController master) throws Exception { + BrokerIdentity brokerIdentity = new BrokerIdentity(master.getBrokerConfig().getBrokerClusterName(), + master.getBrokerConfig().getBrokerName(), slaveBrokerId); + + brokerContainer.removeBroker(brokerIdentity); + } + + protected static void awaitUntilSlaveOK() { + await().atMost(100, TimeUnit.SECONDS) + .until(() -> { + boolean isOk = master1With3Replicas.getMessageStore().getHaService().getConnectionCount().get() == 2 + && master1With3Replicas.getMessageStore().getAliveReplicaNumInGroup() == 3; + for (HAConnection haConnection : master1With3Replicas.getMessageStore().getHaService().getConnectionList()) { + isOk &= haConnection.getCurrentState().equals(HAConnectionState.TRANSFER); + } + return isOk; + }); + + await().atMost(100, TimeUnit.SECONDS) + .until(() -> { + boolean isOk = master2With3Replicas.getMessageStore().getHaService().getConnectionCount().get() == 2 + && master2With3Replicas.getMessageStore().getAliveReplicaNumInGroup() == 3; + for (HAConnection haConnection : master2With3Replicas.getMessageStore().getHaService().getConnectionList()) { + isOk &= haConnection.getCurrentState().equals(HAConnectionState.TRANSFER); + } + return isOk; + }); + + await().atMost(100, TimeUnit.SECONDS) + .until(() -> { + boolean isOk = master3With3Replicas.getMessageStore().getHaService().getConnectionCount().get() == 2 + && master3With3Replicas.getMessageStore().getAliveReplicaNumInGroup() == 3; + for (HAConnection haConnection : master3With3Replicas.getMessageStore().getHaService().getConnectionList()) { + isOk &= haConnection.getCurrentState().equals(HAConnectionState.TRANSFER); + } + return isOk; + }); + + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + protected static void isolateBroker(BrokerController brokerController) { + final BrokerConfig config = brokerController.getBrokerConfig(); + + BrokerConfigLite liteConfig = new BrokerConfigLite(config.getBrokerClusterName(), + config.getBrokerName(), + brokerController.getBrokerAddr(), + config.getBrokerId()); + + // Reject register requests from the specific broker + isolatedBrokers.putIfAbsent(liteConfig, brokerController); + + // UnRegister the specific broker immediately + namesrvController.getRouteInfoManager().unregisterBroker(liteConfig.getClusterName(), + liteConfig.getBrokerAddr(), + liteConfig.getBrokerName(), + liteConfig.getBrokerId()); + } + + protected static void cancelIsolatedBroker(BrokerController brokerController) { + final BrokerConfig config = brokerController.getBrokerConfig(); + + BrokerConfigLite liteConfig = new BrokerConfigLite(config.getBrokerClusterName(), + config.getBrokerName(), + brokerController.getBrokerAddr(), + config.getBrokerId()); + + isolatedBrokers.remove(liteConfig); + brokerController.registerBrokerAll(true, false, true); + + await().atMost(Duration.ofMinutes(1)).until(() -> namesrvController.getRouteInfoManager() + .getBrokerMemberGroup(liteConfig.getClusterName(), liteConfig.brokerName).getBrokerAddrs() + .containsKey(liteConfig.getBrokerId())); + } + + protected static InnerSalveBrokerController getSlaveFromContainerByName(BrokerContainer brokerContainer, + String brokerName) { + InnerSalveBrokerController targetSlave = null; + for (InnerSalveBrokerController slave : brokerContainer.getSlaveBrokers()) { + if (slave.getBrokerConfig().getBrokerName().equals(brokerName)) { + targetSlave = slave; + } + } + + return targetSlave; + } + + protected static void changeCompatibleMode(boolean compatibleMode) { + brokerContainer1.getBrokerControllers().forEach(brokerController -> brokerController.getBrokerConfig().setCompatibleWithOldNameSrv(compatibleMode)); + brokerContainer2.getBrokerControllers().forEach(brokerController -> brokerController.getBrokerConfig().setCompatibleWithOldNameSrv(compatibleMode)); + brokerContainer3.getBrokerControllers().forEach(brokerController -> brokerController.getBrokerConfig().setCompatibleWithOldNameSrv(compatibleMode)); + } + + protected static Set filterMessageQueue(Set mqSet, String topic) { + Set targetMqSet = new HashSet<>(); + if (topic != null) { + for (MessageQueue mq : mqSet) { + if (mq.getTopic().equals(topic)) { + targetMqSet.add(mq); + } + } + } + + return targetMqSet; + } + + public static class BrokerGroupConfig { + int totalReplicas = 3; + int minReplicas = 1; + int inSyncReplicas = 2; + boolean autoReplicas = true; + boolean enableSlaveActingMaster = true; + boolean enableRemoteEscape = true; + boolean slaveReadEnable = true; + + public BrokerGroupConfig() { + } + + public BrokerGroupConfig(final int totalReplicas, final int minReplicas, final int inSyncReplicas, + final boolean autoReplicas, boolean enableSlaveActingMaster, boolean slaveReadEnable) { + this.totalReplicas = totalReplicas; + this.minReplicas = minReplicas; + this.inSyncReplicas = inSyncReplicas; + this.autoReplicas = autoReplicas; + this.enableSlaveActingMaster = enableSlaveActingMaster; + this.slaveReadEnable = slaveReadEnable; + } + } + + static class BrokerConfigLite { + private String clusterName; + private String brokerName; + private String brokerAddr; + private long brokerId; + + public BrokerConfigLite(final String clusterName, final String brokerName, final String brokerAddr, + final long brokerId) { + this.clusterName = clusterName; + this.brokerName = brokerName; + this.brokerAddr = brokerAddr; + this.brokerId = brokerId; + } + + public String getClusterName() { + return clusterName; + } + + public String getBrokerName() { + return brokerName; + } + + public String getBrokerAddr() { + return brokerAddr; + } + + public long getBrokerId() { + return brokerId; + } + + @Override + public boolean equals(final Object o) { + if (this == o) + return true; + + if (o == null || getClass() != o.getClass()) + return false; + + final BrokerConfigLite lite = (BrokerConfigLite) o; + + return new EqualsBuilder() + .append(clusterName, lite.clusterName) + .append(brokerName, lite.brokerName) + .append(brokerAddr, lite.brokerAddr) + .append(brokerId, lite.brokerId) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(clusterName) + .append(brokerName) + .append(brokerAddr) + .append(brokerId) + .toHashCode(); + } + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/container/GetMaxOffsetFromSlaveIT.java b/test/src/test/java/org/apache/rocketmq/test/container/GetMaxOffsetFromSlaveIT.java new file mode 100644 index 0000000..37ffa3d --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/container/GetMaxOffsetFromSlaveIT.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.test.container; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.impl.producer.TopicPublishInfo; +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.common.message.MessageQueue; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@Ignore +public class GetMaxOffsetFromSlaveIT extends ContainerIntegrationTestBase { + private static DefaultMQProducer mqProducer; + + private static final String MSG = "Hello RocketMQ "; + private static final byte[] MESSAGE_BODY = MSG.getBytes(StandardCharsets.UTF_8); + + public GetMaxOffsetFromSlaveIT() { + } + + @BeforeClass + public static void beforeClass() throws MQClientException { + mqProducer = createProducer(GetMaxOffsetFromSlaveIT.class.getSimpleName() + "_Producer"); + mqProducer.start(); + } + + @AfterClass + public static void afterClass() { + if (mqProducer != null) { + mqProducer.shutdown(); + } + } + + @Test + public void testGetMaxOffsetFromSlave() throws InterruptedException, RemotingException, MQClientException, MQBrokerException { + awaitUntilSlaveOK(); + mqProducer.getDefaultMQProducerImpl().getmQClientFactory().updateTopicRouteInfoFromNameServer(THREE_REPLICAS_TOPIC); + + for (int i = 0; i < 100; i++) { + Message msg = new Message(THREE_REPLICAS_TOPIC, MESSAGE_BODY); + SendResult sendResult = mqProducer.send(msg, 10000); + assertThat(sendResult.getSendStatus()).isEqualTo(SendStatus.SEND_OK); + } + + Map maxOffsetMap = new HashMap<>(); + TopicPublishInfo publishInfo = mqProducer.getDefaultMQProducerImpl().getTopicPublishInfoTable().get(THREE_REPLICAS_TOPIC); + assertThat(publishInfo).isNotNull(); + for (MessageQueue mq : publishInfo.getMessageQueueList()) { + maxOffsetMap.put(mq.getQueueId(), mqProducer.getDefaultMQProducerImpl(). + maxOffset(new MessageQueue(THREE_REPLICAS_TOPIC, master3With3Replicas.getBrokerConfig().getBrokerName(), mq.getQueueId()))); + } + + isolateBroker(master3With3Replicas); + + mqProducer.getDefaultMQProducerImpl().getmQClientFactory().updateTopicRouteInfoFromNameServer(THREE_REPLICAS_TOPIC); + assertThat(mqProducer.getDefaultMQProducerImpl().getmQClientFactory().findBrokerAddressInPublish( + master3With3Replicas.getBrokerConfig().getBrokerName())).isNotNull(); + + for (MessageQueue mq : publishInfo.getMessageQueueList()) { + assertThat(mqProducer.getDefaultMQProducerImpl().maxOffset( + new MessageQueue(THREE_REPLICAS_TOPIC, master3With3Replicas.getBrokerConfig().getBrokerName(), mq.getQueueId()))) + .isEqualTo(maxOffsetMap.get(mq.getQueueId())); + } + + cancelIsolatedBroker(master3With3Replicas); + await().atMost(100, TimeUnit.SECONDS) + .until(() -> ((DefaultMessageStore) master3With3Replicas.getMessageStore()).getHaService().getConnectionCount().get() == 2); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/container/GetMetadataReverseIT.java b/test/src/test/java/org/apache/rocketmq/test/container/GetMetadataReverseIT.java new file mode 100644 index 0000000..8df77ac --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/container/GetMetadataReverseIT.java @@ -0,0 +1,289 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.test.container; + +import java.time.Duration; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.rocketmq.container.InnerSalveBrokerController; +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.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.BrokerIdentity; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageQueue; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +import static org.awaitility.Awaitility.await; + +@Ignore +public class GetMetadataReverseIT extends ContainerIntegrationTestBase { + + private static DefaultMQProducer producer; + + private static final String CONSUMER_GROUP = GetMetadataReverseIT.class.getSimpleName() + "_Consumer"; + + private static final int MESSAGE_COUNT = 32; + + private final Random random = new Random(); + + public GetMetadataReverseIT() { + + } + + @BeforeClass + public static void beforeClass() throws Throwable { + producer = createProducer(PushMultipleReplicasIT.class.getSimpleName() + "_PRODUCER"); + producer.setSendMsgTimeout(15 * 1000); + producer.start(); + } + + @AfterClass + public static void afterClass() throws Exception { + producer.shutdown(); + } + + @Test + public void testGetMetadataReverse_consumerOffset() throws Exception { + String topic = GetMetadataReverseIT.class.getSimpleName() + "_consumerOffset" + random.nextInt(65535); + createTopicTo(master1With3Replicas, topic, 1, 1); + // Wait topic synchronization + await().atMost(Duration.ofMinutes(1)).until(() -> { + InnerSalveBrokerController slaveBroker = brokerContainer2.getSlaveBrokers().iterator().next(); + return slaveBroker.getTopicConfigManager().selectTopicConfig(topic) != null; + }); + + int sendSuccess = 0; + for (int i = 0; i < MESSAGE_COUNT; i++) { + Message msg = new Message(topic, Integer.toString(i).getBytes()); + SendResult sendResult = producer.send(msg); + if (sendResult.getSendStatus() == SendStatus.SEND_OK) { + sendSuccess++; + } + } + final int finalSendSuccess = sendSuccess; + await().atMost(Duration.ofMinutes(1)).until(() -> finalSendSuccess >= MESSAGE_COUNT); + + isolateBroker(master1With3Replicas); + brokerContainer1.removeBroker(new BrokerIdentity( + master1With3Replicas.getBrokerConfig().getBrokerClusterName(), + master1With3Replicas.getBrokerConfig().getBrokerName(), + master1With3Replicas.getBrokerConfig().getBrokerId())); + + DefaultMQPushConsumer pushConsumer = createPushConsumer(CONSUMER_GROUP); + pushConsumer.subscribe(topic, "*"); + pushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + AtomicInteger receivedMsgCount = new AtomicInteger(0); + pushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + receivedMsgCount.addAndGet(msgs.size()); + msgs.forEach(x -> System.out.printf(x + "%n")); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + pushConsumer.start(); + await().atMost(Duration.ofMinutes(3)).until(() -> receivedMsgCount.get() >= MESSAGE_COUNT); + + await().atMost(Duration.ofMinutes(1)).until(() -> { + pushConsumer.getDefaultMQPushConsumerImpl().persistConsumerOffset(); + Map slaveOffsetTable = null; + for (InnerSalveBrokerController slave : brokerContainer2.getSlaveBrokers()) { + if (slave.getBrokerConfig().getBrokerName().equals(master1With3Replicas.getBrokerConfig().getBrokerName())) { + slaveOffsetTable = slave.getConsumerOffsetManager().queryOffset(CONSUMER_GROUP, topic); + } + } + + if (slaveOffsetTable != null) { + long totalOffset = 0; + for (final Long offset : slaveOffsetTable.values()) { + totalOffset += offset; + } + + return totalOffset >= MESSAGE_COUNT; + } + return false; + }); + + //Add back master + master1With3Replicas = brokerContainer1.addBroker(master1With3Replicas.getBrokerConfig(), master1With3Replicas.getMessageStoreConfig()); + master1With3Replicas.start(); + cancelIsolatedBroker(master1With3Replicas); + + awaitUntilSlaveOK(); + + await().atMost(Duration.ofMinutes(1)).until(() -> { + Map offsetTable = master1With3Replicas.getConsumerOffsetManager().queryOffset(CONSUMER_GROUP, topic); + long totalOffset = 0; + if (offsetTable != null) { + for (final Long offset : offsetTable.values()) { + totalOffset += offset; + } + } + return totalOffset >= MESSAGE_COUNT; + }); + + pushConsumer.shutdown(); + } + + @Test + public void testGetMetadataReverse_delayOffset() throws Exception { + String topic = GetMetadataReverseIT.class.getSimpleName() + "_delayOffset" + random.nextInt(65535); + createTopicTo(master1With3Replicas, topic, 1, 1); + createTopicTo(master2With3Replicas, topic, 1, 1); + createTopicTo(master3With3Replicas, topic, 1, 1); + // Wait topic synchronization + await().atMost(Duration.ofMinutes(1)).until(() -> { + InnerSalveBrokerController slaveBroker = brokerContainer2.getSlaveBrokers().iterator().next(); + return slaveBroker.getTopicConfigManager().selectTopicConfig(topic) != null; + }); + int delayLevel = 4; + + DefaultMQPushConsumer pushConsumer = createPushConsumer(CONSUMER_GROUP); + pushConsumer.subscribe(topic, "*"); + AtomicInteger receivedMsgCount = new AtomicInteger(0); + pushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + receivedMsgCount.addAndGet(msgs.size()); + msgs.forEach(x -> System.out.printf(x + "%n")); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + pushConsumer.start(); + + MessageQueue messageQueue = new MessageQueue(topic, master1With3Replicas.getBrokerConfig().getBrokerName(), 0); + int sendSuccess = 0; + for (int i = 0; i < MESSAGE_COUNT; i++) { + Message msg = new Message(topic, Integer.toString(i).getBytes()); + msg.setDelayTimeLevel(delayLevel); + SendResult sendResult = producer.send(msg, messageQueue); + if (sendResult.getSendStatus() == SendStatus.SEND_OK) { + sendSuccess++; + } + } + final int finalSendSuccess = sendSuccess; + await().atMost(Duration.ofMinutes(1)).until(() -> finalSendSuccess >= MESSAGE_COUNT); + + isolateBroker(master1With3Replicas); + brokerContainer1.removeBroker(new BrokerIdentity( + master1With3Replicas.getBrokerConfig().getBrokerClusterName(), + master1With3Replicas.getBrokerConfig().getBrokerName(), + master1With3Replicas.getBrokerConfig().getBrokerId())); + + await().atMost(Duration.ofMinutes(1)).until(() -> receivedMsgCount.get() >= MESSAGE_COUNT); + + await().atMost(Duration.ofMinutes(1)).until(() -> { + pushConsumer.getDefaultMQPushConsumerImpl().persistConsumerOffset(); + Map offsetTable = master2With3Replicas.getConsumerOffsetManager().queryOffset(CONSUMER_GROUP, topic); + if (offsetTable != null) { + long totalOffset = 0; + for (final Long offset : offsetTable.values()) { + totalOffset += offset; + } + return totalOffset >= MESSAGE_COUNT; + + } + return false; + }); + + //Add back master + master1With3Replicas = brokerContainer1.addBroker(master1With3Replicas.getBrokerConfig(), master1With3Replicas.getMessageStoreConfig()); + master1With3Replicas.start(); + cancelIsolatedBroker(master1With3Replicas); + + awaitUntilSlaveOK(); + + await().atMost(Duration.ofMinutes(1)).until(() -> { + Map offsetTable = master1With3Replicas.getScheduleMessageService().getOffsetTable(); + return offsetTable.get(delayLevel) >= MESSAGE_COUNT; + }); + + pushConsumer.shutdown(); + } + + @Test + public void testGetMetadataReverse_timerCheckPoint() throws Exception { + String topic = GetMetadataReverseIT.class.getSimpleName() + "_timerCheckPoint" + random.nextInt(65535); + createTopicTo(master1With3Replicas, topic, 1, 1); + createTopicTo(master2With3Replicas, topic, 1, 1); + createTopicTo(master3With3Replicas, topic, 1, 1); + // Wait topic synchronization + await().atMost(Duration.ofMinutes(1)).until(() -> { + InnerSalveBrokerController slaveBroker = brokerContainer2.getSlaveBrokers().iterator().next(); + return slaveBroker.getTopicConfigManager().selectTopicConfig(topic) != null; + }); + + DefaultMQPushConsumer pushConsumer = createPushConsumer(CONSUMER_GROUP); + pushConsumer.subscribe(topic, "*"); + AtomicInteger receivedMsgCount = new AtomicInteger(0); + pushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + receivedMsgCount.addAndGet(msgs.size()); + msgs.forEach(x -> System.out.printf(x + "%n")); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + pushConsumer.start(); + + MessageQueue messageQueue = new MessageQueue(topic, master1With3Replicas.getBrokerConfig().getBrokerName(), 0); + int sendSuccess = 0; + for (int i = 0; i < MESSAGE_COUNT; i++) { + Message msg = new Message(topic, Integer.toString(i).getBytes()); + msg.setDelayTimeSec(30); + SendResult sendResult = producer.send(msg, messageQueue); + if (sendResult.getSendStatus() == SendStatus.SEND_OK) { + sendSuccess++; + } + } + final int finalSendSuccess = sendSuccess; + await().atMost(Duration.ofMinutes(1)).until(() -> finalSendSuccess >= MESSAGE_COUNT); + + isolateBroker(master1With3Replicas); + brokerContainer1.removeBroker(new BrokerIdentity( + master1With3Replicas.getBrokerConfig().getBrokerClusterName(), + master1With3Replicas.getBrokerConfig().getBrokerName(), + master1With3Replicas.getBrokerConfig().getBrokerId())); + + await().atMost(Duration.ofMinutes(1)).until(() -> receivedMsgCount.get() >= MESSAGE_COUNT); + + await().atMost(Duration.ofMinutes(1)).until(() -> { + pushConsumer.getDefaultMQPushConsumerImpl().persistConsumerOffset(); + Map offsetTable = master2With3Replicas.getConsumerOffsetManager().queryOffset(CONSUMER_GROUP, topic); + if (offsetTable != null) { + long totalOffset = 0; + for (final Long offset : offsetTable.values()) { + totalOffset += offset; + } + return totalOffset >= MESSAGE_COUNT; + } + return false; + }); + + //Add back master + master1With3Replicas = brokerContainer1.addBroker(master1With3Replicas.getBrokerConfig(), master1With3Replicas.getMessageStoreConfig()); + master1With3Replicas.start(); + cancelIsolatedBroker(master1With3Replicas); + + awaitUntilSlaveOK(); + + await().atMost(Duration.ofMinutes(1)).until(() -> master1With3Replicas.getTimerCheckpoint().getMasterTimerQueueOffset() >= MESSAGE_COUNT); + + pushConsumer.shutdown(); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/container/PopSlaveActingMasterIT.java b/test/src/test/java/org/apache/rocketmq/test/container/PopSlaveActingMasterIT.java new file mode 100644 index 0000000..fe40e86 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/container/PopSlaveActingMasterIT.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.test.container; + +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.rocketmq.broker.BrokerController; +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.producer.DefaultMQProducer; +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.BrokerIdentity; +import org.apache.rocketmq.common.KeyBuilder; +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.common.message.MessageRequestMode; +import org.apache.rocketmq.container.BrokerContainer; +import org.apache.rocketmq.container.InnerBrokerController; +import org.apache.rocketmq.container.InnerSalveBrokerController; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@Ignore +public class PopSlaveActingMasterIT extends ContainerIntegrationTestBase { + private static final String CONSUME_GROUP = PopSlaveActingMasterIT.class.getSimpleName() + "_Consumer"; + private final static int MESSAGE_COUNT = 16; + private final Random random = new Random(); + private static DefaultMQProducer producer; + private final static String MESSAGE_STRING = RandomStringUtils.random(1024); + private static final byte[] MESSAGE_BODY = MESSAGE_STRING.getBytes(StandardCharsets.UTF_8); + private final BrokerConfig brokerConfig = new BrokerConfig(); + + public PopSlaveActingMasterIT() { + } + + void createTopic(String topic) { + createTopicTo(master1With3Replicas, topic, 1, 1); + createTopicTo(master2With3Replicas, topic, 1, 1); + createTopicTo(master3With3Replicas, topic, 1, 1); + } + + @BeforeClass + public static void beforeClass() throws Throwable { + producer = createProducer(PopSlaveActingMasterIT.class.getSimpleName() + "_PRODUCER"); + producer.setSendMsgTimeout(5000); + producer.start(); + } + + @AfterClass + public static void afterClass() throws Exception { + producer.shutdown(); + } + + + @Test + public void testLocalActing_ackSlave() throws Exception { + String topic = PopSlaveActingMasterIT.class.getSimpleName() + random.nextInt(65535); + createTopic(topic); + String retryTopic = KeyBuilder.buildPopRetryTopic(topic, CONSUME_GROUP, brokerConfig.isEnableRetryTopicV2()); + createTopic(retryTopic); + + this.switchPop(topic); + + producer.getDefaultMQProducerImpl().getmQClientFactory().updateTopicRouteInfoFromNameServer(topic); + + MessageQueue messageQueue = new MessageQueue(topic, master1With3Replicas.getBrokerConfig().getBrokerName(), 0); + int sendSuccess = 0; + for (int i = 0; i < MESSAGE_COUNT; i++) { + Message msg = new Message(topic, MESSAGE_BODY); + SendResult sendResult = producer.send(msg, messageQueue); + if (sendResult.getSendStatus() == SendStatus.SEND_OK) { + sendSuccess++; + } + } + + final int finalSendSuccess = sendSuccess; + await().atMost(Duration.ofMinutes(1)).until(() -> finalSendSuccess >= MESSAGE_COUNT); + + isolateBroker(master1With3Replicas); + + DefaultMQPushConsumer consumer = createPushConsumer(CONSUME_GROUP); + consumer.subscribe(topic, "*"); + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + List consumedMessages = new CopyOnWriteArrayList<>(); + consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + msgs.forEach(msg -> { + consumedMessages.add(msg.getMsgId()); + }); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + consumer.setClientRebalance(false); + consumer.start(); + + await().atMost(Duration.ofMinutes(1)).until(() -> consumedMessages.size() >= MESSAGE_COUNT); + + consumer.shutdown(); + + List retryMsgList = new CopyOnWriteArrayList<>(); + DefaultMQPushConsumer pushConsumer = createPushConsumer(CONSUME_GROUP); + pushConsumer.subscribe(retryTopic, "*"); + pushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + for (MessageExt msg : msgs) { + retryMsgList.add(new String(msg.getBody())); + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + pushConsumer.start(); + + Thread.sleep(10000L); + + assertThat(retryMsgList.size()).isEqualTo(0); + + cancelIsolatedBroker(master1With3Replicas); + awaitUntilSlaveOK(); + + pushConsumer.shutdown(); + } + + @Test + public void testLocalActing_notAckSlave() throws Exception { + String topic = PopSlaveActingMasterIT.class.getSimpleName() + random.nextInt(65535); + createTopic(topic); + String retryTopic = KeyBuilder.buildPopRetryTopic(topic, CONSUME_GROUP, brokerConfig.isEnableRetryTopicV2()); + createTopic(retryTopic); + + this.switchPop(topic); + + producer.getDefaultMQProducerImpl().getmQClientFactory().updateTopicRouteInfoFromNameServer(topic); + + Set sendToIsolateMsgSet = new HashSet<>(); + MessageQueue messageQueue = new MessageQueue(topic, master1With3Replicas.getBrokerConfig().getBrokerName(), 0); + int sendSuccess = 0; + for (int i = 0; i < MESSAGE_COUNT; i++) { + Message msg = new Message(topic, MESSAGE_BODY); + SendResult sendResult = producer.send(msg, messageQueue); + if (sendResult.getSendStatus() == SendStatus.SEND_OK) { + sendToIsolateMsgSet.add(new String(msg.getBody())); + sendSuccess++; + } + } + + System.out.printf("send success %d%n", sendSuccess); + final int finalSendSuccess = sendSuccess; + await().atMost(Duration.ofMinutes(1)).until(() -> finalSendSuccess >= MESSAGE_COUNT); + + isolateBroker(master1With3Replicas); + System.out.printf("isolate master1%n"); + + DefaultMQPushConsumer consumer = createPushConsumer(CONSUME_GROUP); + consumer.subscribe(topic, "*"); + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + consumer.setPopInvisibleTime(5000L); + List consumedMessages = new CopyOnWriteArrayList<>(); + consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + msgs.forEach(msg -> { + msg.setReconsumeTimes(0); + consumedMessages.add(msg.getMsgId()); + }); + return ConsumeConcurrentlyStatus.RECONSUME_LATER; + }); + consumer.setClientRebalance(false); + consumer.start(); + + await().atMost(Duration.ofMinutes(1)).until(() -> consumedMessages.size() >= MESSAGE_COUNT); + consumer.shutdown(); + + List retryMsgList = new CopyOnWriteArrayList<>(); + DefaultMQPushConsumer pushConsumer = createPushConsumer(CONSUME_GROUP); + pushConsumer.subscribe(retryTopic, "*"); + pushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + pushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + for (MessageExt msg : msgs) { + retryMsgList.add(new String(msg.getBody())); + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + pushConsumer.start(); + + AtomicInteger failCnt = new AtomicInteger(0); + await().atMost(Duration.ofMinutes(3)).pollInterval(Duration.ofSeconds(10)).until(() -> { + if (retryMsgList.size() < MESSAGE_COUNT) { + return false; + } + + for (String msgBodyString : retryMsgList) { + if (!sendToIsolateMsgSet.contains(msgBodyString)) { + return false; + } + } + return true; + }); + + cancelIsolatedBroker(master1With3Replicas); + awaitUntilSlaveOK(); + + pushConsumer.shutdown(); + } + + @Test + public void testRemoteActing_ackSlave() throws Exception { + String topic = PopSlaveActingMasterIT.class.getSimpleName() + random.nextInt(65535); + createTopic(topic); + String retryTopic = KeyBuilder.buildPopRetryTopic(topic, CONSUME_GROUP, brokerConfig.isEnableRetryTopicV2()); + createTopic(retryTopic); + + switchPop(topic); + + producer.getDefaultMQProducerImpl().getmQClientFactory().updateTopicRouteInfoFromNameServer(topic); + + MessageQueue messageQueue = new MessageQueue(topic, master1With3Replicas.getBrokerConfig().getBrokerName(), 0); + int sendSuccess = 0; + for (int i = 0; i < MESSAGE_COUNT; i++) { + Message msg = new Message(topic, MESSAGE_BODY); + SendResult sendResult = producer.send(msg, messageQueue); + if (sendResult.getSendStatus() == SendStatus.SEND_OK) { + sendSuccess++; + } + } + + final int finalSendSuccess = sendSuccess; + await().atMost(Duration.ofMinutes(1)).until(() -> finalSendSuccess >= MESSAGE_COUNT); + + isolateBroker(master1With3Replicas); + + isolateBroker(master2With3Replicas); + brokerContainer2.removeBroker(new BrokerIdentity( + master2With3Replicas.getBrokerConfig().getBrokerClusterName(), + master2With3Replicas.getBrokerConfig().getBrokerName(), + master2With3Replicas.getBrokerConfig().getBrokerId())); + + DefaultMQPushConsumer consumer = createPushConsumer(CONSUME_GROUP); + consumer.subscribe(topic, "*"); + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + List consumedMessages = new CopyOnWriteArrayList<>(); + consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + msgs.forEach(msg -> { + consumedMessages.add(msg.getMsgId()); + }); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + consumer.setClientRebalance(false); + consumer.start(); + + await().atMost(Duration.ofMinutes(2)).until(() -> consumedMessages.size() >= MESSAGE_COUNT); + consumer.shutdown(); + + List retryMsgList = new CopyOnWriteArrayList<>(); + DefaultMQPushConsumer pushConsumer = createPushConsumer(CONSUME_GROUP); + pushConsumer.subscribe(retryTopic, "*"); + pushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + for (MessageExt msg : msgs) { + retryMsgList.add(new String(msg.getBody())); + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + pushConsumer.start(); + + Thread.sleep(10000); + + assertThat(retryMsgList.size()).isEqualTo(0); + + cancelIsolatedBroker(master1With3Replicas); + + //Add back master + master2With3Replicas = brokerContainer2.addBroker(master2With3Replicas.getBrokerConfig(), master2With3Replicas.getMessageStoreConfig()); + master2With3Replicas.start(); + cancelIsolatedBroker(master2With3Replicas); + + awaitUntilSlaveOK(); + + Thread.sleep(10000); + + assertThat(retryMsgList.size()).isEqualTo(0); + + pushConsumer.shutdown(); + } + + @Test + public void testRemoteActing_notAckSlave_getFromLocal() throws Exception { + String topic = PopSlaveActingMasterIT.class.getSimpleName() + random.nextInt(65535); + createTopic(topic); + this.switchPop(topic); + + String retryTopic = KeyBuilder.buildPopRetryTopic(topic, CONSUME_GROUP, brokerConfig.isEnableRetryTopicV2()); + createTopic(retryTopic); + + producer.getDefaultMQProducerImpl().getmQClientFactory().updateTopicRouteInfoFromNameServer(topic); + + Set sendToIsolateMsgSet = new HashSet<>(); + MessageQueue messageQueue = new MessageQueue(topic, master1With3Replicas.getBrokerConfig().getBrokerName(), 0); + int sendSuccess = 0; + for (int i = 0; i < MESSAGE_COUNT; i++) { + Message msg = new Message(topic, MESSAGE_BODY); + SendResult sendResult = producer.send(msg, messageQueue); + if (sendResult.getSendStatus() == SendStatus.SEND_OK) { + sendToIsolateMsgSet.add(new String(msg.getBody())); + sendSuccess++; + } + } + + final int finalSendSuccess = sendSuccess; + await().atMost(Duration.ofMinutes(1)).until(() -> finalSendSuccess >= MESSAGE_COUNT); + + isolateBroker(master1With3Replicas); + + isolateBroker(master2With3Replicas); + brokerContainer2.removeBroker(new BrokerIdentity( + master2With3Replicas.getBrokerConfig().getBrokerClusterName(), + master2With3Replicas.getBrokerConfig().getBrokerName(), + master2With3Replicas.getBrokerConfig().getBrokerId())); + + + DefaultMQPushConsumer consumer = createPushConsumer(CONSUME_GROUP); + consumer.subscribe(topic, "*"); + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + List consumedMessages = new CopyOnWriteArrayList<>(); + consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + msgs.forEach(msg -> { + consumedMessages.add(msg.getMsgId()); + }); + return ConsumeConcurrentlyStatus.RECONSUME_LATER; + }); + consumer.setClientRebalance(false); + consumer.start(); + + await().atMost(Duration.ofMinutes(3)).until(() -> consumedMessages.size() >= MESSAGE_COUNT); + consumer.shutdown(); + + + List retryMsgList = new CopyOnWriteArrayList<>(); + DefaultMQPushConsumer pushConsumer = createPushConsumer(CONSUME_GROUP); + pushConsumer.subscribe(retryTopic, "*"); + pushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + for (MessageExt msg : msgs) { + retryMsgList.add(new String(msg.getBody())); + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + pushConsumer.start(); + + + await().atMost(Duration.ofMinutes(1)).until(() -> { + if (retryMsgList.size() < MESSAGE_COUNT) { + return false; + } + + for (String msgBodyString : retryMsgList) { + if (!sendToIsolateMsgSet.contains(msgBodyString)) { + return false; + } + } + return true; + }); + + cancelIsolatedBroker(master1With3Replicas); + + //Add back master + master2With3Replicas = brokerContainer2.addBroker(master2With3Replicas.getBrokerConfig(), master2With3Replicas.getMessageStoreConfig()); + master2With3Replicas.start(); + cancelIsolatedBroker(master2With3Replicas); + + awaitUntilSlaveOK(); + pushConsumer.shutdown(); + } + + @Test + public void testRemoteActing_notAckSlave_getFromRemote() throws Exception { + String topic = PopSlaveActingMasterIT.class.getSimpleName() + random.nextInt(65535); + createTopic(topic); + this.switchPop(topic); + String retryTopic = KeyBuilder.buildPopRetryTopic(topic, CONSUME_GROUP, brokerConfig.isEnableRetryTopicV2()); + createTopic(retryTopic); + + producer.getDefaultMQProducerImpl().getmQClientFactory().updateTopicRouteInfoFromNameServer(topic); + + Set sendToIsolateMsgSet = new HashSet<>(); + MessageQueue messageQueue = new MessageQueue(topic, master1With3Replicas.getBrokerConfig().getBrokerName(), 0); + int sendSuccess = 0; + for (int i = 0; i < MESSAGE_COUNT; i++) { + Message msg = new Message(topic, MESSAGE_BODY); + SendResult sendResult = producer.send(msg, messageQueue); + if (sendResult.getSendStatus() == SendStatus.SEND_OK) { + sendToIsolateMsgSet.add(new String(msg.getBody())); + sendSuccess++; + } + } + + final int finalSendSuccess = sendSuccess; + await().atMost(Duration.ofMinutes(1)).until(() -> finalSendSuccess >= MESSAGE_COUNT); + + isolateBroker(master1With3Replicas); + + isolateBroker(master2With3Replicas); + brokerContainer2.removeBroker(new BrokerIdentity( + master2With3Replicas.getBrokerConfig().getBrokerClusterName(), + master2With3Replicas.getBrokerConfig().getBrokerName(), + master2With3Replicas.getBrokerConfig().getBrokerId())); + + BrokerController slave1InBrokerContainer3 = getSlaveFromContainerByName(brokerContainer3, master1With3Replicas.getBrokerConfig().getBrokerName()); + isolateBroker(slave1InBrokerContainer3); + brokerContainer3.removeBroker(new BrokerIdentity( + slave1InBrokerContainer3.getBrokerConfig().getBrokerClusterName(), + slave1InBrokerContainer3.getBrokerConfig().getBrokerName(), + slave1InBrokerContainer3.getBrokerConfig().getBrokerId())); + + DefaultMQPushConsumer consumer = createPushConsumer(CONSUME_GROUP); + consumer.subscribe(topic, "*"); + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + List consumedMessages = new CopyOnWriteArrayList<>(); + consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + msgs.forEach(msg -> { + consumedMessages.add(msg.getMsgId()); + }); + return ConsumeConcurrentlyStatus.RECONSUME_LATER; + }); + consumer.setClientRebalance(false); + consumer.start(); + + await().atMost(Duration.ofMinutes(1)).until(() -> consumedMessages.size() >= MESSAGE_COUNT); + consumer.shutdown(); + + + List retryMsgList = new CopyOnWriteArrayList<>(); + DefaultMQPushConsumer pushConsumer = createPushConsumer(CONSUME_GROUP); + pushConsumer.subscribe(retryTopic, "*"); + pushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + for (MessageExt msg : msgs) { + retryMsgList.add(new String(msg.getBody())); + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + pushConsumer.start(); + + Thread.sleep(10000); + + await().atMost(Duration.ofMinutes(1)).until(() -> { + if (retryMsgList.size() < MESSAGE_COUNT) { + return false; + } + + for (String msgBodyString : retryMsgList) { + if (!sendToIsolateMsgSet.contains(msgBodyString)) { + return false; + } + } + return true; + }); + + cancelIsolatedBroker(master1With3Replicas); + + //Add back master + master2With3Replicas = brokerContainer2.addBroker(master2With3Replicas.getBrokerConfig(), master2With3Replicas.getMessageStoreConfig()); + master2With3Replicas.start(); + cancelIsolatedBroker(master2With3Replicas); + + //Add back slave1 to container3 + slave1InBrokerContainer3 = brokerContainer3.addBroker(slave1InBrokerContainer3.getBrokerConfig(), slave1InBrokerContainer3.getMessageStoreConfig()); + slave1InBrokerContainer3.start(); + cancelIsolatedBroker(slave1InBrokerContainer3); + + awaitUntilSlaveOK(); + pushConsumer.shutdown(); + } + + private void switchPop(String topic) throws Exception { + for (BrokerContainer brokerContainer : brokerContainerList) { + for (InnerBrokerController master : brokerContainer.getMasterBrokers()) { + String brokerAddr = master.getBrokerAddr(); + defaultMQAdminExt.setMessageRequestMode(brokerAddr, topic, CONSUME_GROUP, MessageRequestMode.POP, 8, 60_000); + } + for (InnerSalveBrokerController slave : brokerContainer.getSlaveBrokers()) { + defaultMQAdminExt.setMessageRequestMode(slave.getBrokerAddr(), topic, CONSUME_GROUP, MessageRequestMode.POP, 8, 60_000); + } + } + + } + +} diff --git a/test/src/test/java/org/apache/rocketmq/test/container/PullMultipleReplicasIT.java b/test/src/test/java/org/apache/rocketmq/test/container/PullMultipleReplicasIT.java new file mode 100644 index 0000000..b87869b --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/container/PullMultipleReplicasIT.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.test.container; + +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.List; + +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.rocketmq.client.impl.consumer.DefaultMQPullConsumerImpl; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.container.InnerSalveBrokerController; +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +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.producer.DefaultMQProducer; +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.message.Message; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@Ignore +public class PullMultipleReplicasIT extends ContainerIntegrationTestBase { + private static DefaultMQPullConsumer pullConsumer; + private static DefaultMQProducer producer; + private static MQClientInstance mqClientInstance; + + private static final String MESSAGE_STRING = RandomStringUtils.random(1024); + private static final byte[] MESSAGE_BODY = MESSAGE_STRING.getBytes(StandardCharsets.UTF_8); + + public PullMultipleReplicasIT() { + } + + @BeforeClass + public static void beforeClass() throws Exception { + + pullConsumer = createPullConsumer(PullMultipleReplicasIT.class.getSimpleName() + "_Consumer"); + pullConsumer.start(); + + Field field = DefaultMQPullConsumerImpl.class.getDeclaredField("mQClientFactory"); + field.setAccessible(true); + mqClientInstance = (MQClientInstance) field.get(pullConsumer.getDefaultMQPullConsumerImpl()); + + producer = createProducer(PullMultipleReplicasIT.class.getSimpleName() + "_Producer"); + producer.setSendMsgTimeout(15 * 1000); + producer.start(); + } + + @AfterClass + public static void afterClass() { + producer.shutdown(); + pullConsumer.shutdown(); + } + + @Test + public void testPullMessageFromSlave() throws InterruptedException, RemotingException, MQClientException, MQBrokerException, UnsupportedEncodingException { + awaitUntilSlaveOK(); + + Message msg = new Message(THREE_REPLICAS_TOPIC, MESSAGE_BODY); + SendResult sendResult = producer.send(msg); + assertThat(sendResult.getSendStatus()).isEqualTo(SendStatus.SEND_OK); + + final MessageQueue messageQueue = sendResult.getMessageQueue(); + final long queueOffset = sendResult.getQueueOffset(); + + final PullResult[] pullResult = {null}; + await().atMost(Duration.ofSeconds(5)).until(() -> { + pullResult[0] = pullConsumer.pull(messageQueue, "*", queueOffset, 1); + return pullResult[0].getPullStatus() == PullStatus.FOUND; + }); + + List msgFoundList = pullResult[0].getMsgFoundList(); + assertThat(msgFoundList.size()).isEqualTo(1); + assertThat(new String(msgFoundList.get(0).getBody(), RemotingHelper.DEFAULT_CHARSET)).isEqualTo(MESSAGE_STRING); + + // Pull the same message from the slave broker + pullConsumer.getDefaultMQPullConsumerImpl().getPullAPIWrapper().updatePullFromWhichNode(messageQueue, 1); + + await().atMost(Duration.ofSeconds(5)).until(() -> { + pullResult[0] = pullConsumer.pull(messageQueue, "*", queueOffset, 1); + return pullResult[0].getPullStatus() == PullStatus.FOUND; + }); + + msgFoundList = pullResult[0].getMsgFoundList(); + assertThat(msgFoundList.size()).isEqualTo(1); + assertThat(new String(msgFoundList.get(0).getBody(), RemotingHelper.DEFAULT_CHARSET)).isEqualTo(MESSAGE_STRING); + + // Pull the same message from the slave broker + pullConsumer.getDefaultMQPullConsumerImpl().getPullAPIWrapper().updatePullFromWhichNode(messageQueue, 2); + + await().atMost(Duration.ofSeconds(5)).until(() -> { + pullResult[0] = pullConsumer.pull(messageQueue, "*", queueOffset, 1); + return pullResult[0].getPullStatus() == PullStatus.FOUND; + }); + + msgFoundList = pullResult[0].getMsgFoundList(); + assertThat(msgFoundList.size()).isEqualTo(1); + assertThat(new String(msgFoundList.get(0).getBody(), RemotingHelper.DEFAULT_CHARSET)).isEqualTo(MESSAGE_STRING); + + pullConsumer.getDefaultMQPullConsumerImpl().getPullAPIWrapper().updatePullFromWhichNode(messageQueue, 0); + } + + @Test + public void testSendMessageBackToSlave() throws InterruptedException, RemotingException, MQClientException, MQBrokerException, UnsupportedEncodingException { + awaitUntilSlaveOK(); + + String clusterTopic = "TOPIC_ON_BROKER2_AND_BROKER3_FOR_MESSAGE_BACK"; + createTopicTo(master1With3Replicas, clusterTopic); + createTopicTo(master3With3Replicas, clusterTopic); + + Message msg = new Message(clusterTopic, MESSAGE_BODY); + producer.setSendMsgTimeout(10 * 1000); + + final MessageQueue[] selectedQueue = new MessageQueue[1]; + await().atMost(Duration.ofSeconds(5)).until(() -> { + for (final MessageQueue queue : producer.fetchPublishMessageQueues(clusterTopic)) { + if (queue.getBrokerName().equals(master3With3Replicas.getBrokerConfig().getBrokerName())) { + selectedQueue[0] = queue; + } + } + return selectedQueue[0] != null; + }); + + SendResult sendResult = producer.send(msg, selectedQueue[0]); + assertThat(sendResult.getSendStatus()).isEqualTo(SendStatus.SEND_OK); + + final MessageQueue messageQueue = sendResult.getMessageQueue(); + final long queueOffset = sendResult.getQueueOffset(); + + final PullResult[] pullResult = {null}; + await().atMost(Duration.ofSeconds(60)).until(() -> { + pullResult[0] = pullConsumer.pull(messageQueue, "*", queueOffset, 1); + return pullResult[0].getPullStatus() == PullStatus.FOUND; + }); + + await().atMost(Duration.ofSeconds(60)).until(() -> { + DefaultMessageStore messageStore = (DefaultMessageStore) master3With3Replicas.getMessageStore(); + return messageStore.getHaService().inSyncReplicasNums(messageStore.getMaxPhyOffset()) == 3; + }); + + InnerSalveBrokerController slaveBroker = null; + for (InnerSalveBrokerController slave : brokerContainer1.getSlaveBrokers()) { + if (slave.getBrokerConfig().getBrokerName().equals(master3With3Replicas.getBrokerConfig().getBrokerName())) { + slaveBroker = slave; + } + } + + assertThat(slaveBroker).isNotNull(); + + MessageExt backMessage = pullResult[0].getMsgFoundList().get(0); + + // Message will be sent to the master broker(master1With3Replicas) beside a slave broker of master3With3Replicas + backMessage.setStoreHost(new InetSocketAddress(slaveBroker.getBrokerConfig().getBrokerIP1(), slaveBroker.getBrokerConfig().getListenPort())); + pullConsumer.sendMessageBack(backMessage, 0); + + String retryTopic = MixAll.getRetryTopic(pullConsumer.getConsumerGroup()); + // Retry topic only has one queue by default + MessageQueue newMsgQueue = new MessageQueue(retryTopic, master1With3Replicas.getBrokerConfig().getBrokerName(), 0); + await().atMost(Duration.ofSeconds(60)).until(() -> { + pullResult[0] = pullConsumer.pull(newMsgQueue, "*", 0, 1); + return pullResult[0].getPullStatus() == PullStatus.FOUND; + }); + + List msgFoundList = pullResult[0].getMsgFoundList(); + assertThat(msgFoundList.size()).isEqualTo(1); + assertThat(new String(msgFoundList.get(0).getBody(), RemotingHelper.DEFAULT_CHARSET)).isEqualTo(MESSAGE_STRING); + + awaitUntilSlaveOK(); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/container/PushMultipleReplicasIT.java b/test/src/test/java/org/apache/rocketmq/test/container/PushMultipleReplicasIT.java new file mode 100644 index 0000000..801f168 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/container/PushMultipleReplicasIT.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.test.container; + +import java.io.UnsupportedEncodingException; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.rocketmq.container.InnerSalveBrokerController; +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.producer.DefaultMQProducer; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.Message; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +import static org.awaitility.Awaitility.await; + +@Ignore +public class PushMultipleReplicasIT extends ContainerIntegrationTestBase { + private static DefaultMQProducer producer; + + private static final String TOPIC = PushMultipleReplicasIT.class.getSimpleName() + "_TOPIC"; + private static final String REDIRECT_TOPIC = PushMultipleReplicasIT.class.getSimpleName() + "_REDIRECT_TOPIC"; + private static final String CONSUMER_GROUP = PushMultipleReplicasIT.class.getSimpleName() + "_Consumer"; + private static final int MESSAGE_COUNT = 32; + + public PushMultipleReplicasIT() throws UnsupportedEncodingException { + } + + @BeforeClass + public static void beforeClass() throws Throwable { + createTopicTo(master1With3Replicas, TOPIC,1, 1); + producer = createProducer(PushMultipleReplicasIT.class.getSimpleName() + "_PRODUCER"); + producer.setSendMsgTimeout(15 * 1000); + producer.start(); + + for (int i = 0; i < MESSAGE_COUNT; i++) { + producer.send(new Message(TOPIC, Integer.toString(i).getBytes())); + } + + createTopicTo(master3With3Replicas, REDIRECT_TOPIC, 1, 1); + } + + @AfterClass + public static void afterClass() throws Exception { + producer.shutdown(); + } + + @Test + public void consumeMessageFromSlave_PushConsumer() throws MQClientException { + // Wait topic synchronization + await().atMost(Duration.ofMinutes(1)).until(() -> { + InnerSalveBrokerController slaveBroker = brokerContainer2.getSlaveBrokers().iterator().next(); + return slaveBroker.getTopicConfigManager().selectTopicConfig(TOPIC) != null; + }); + isolateBroker(master1With3Replicas); + DefaultMQPushConsumer pushConsumer = createPushConsumer(CONSUMER_GROUP); + pushConsumer.subscribe(TOPIC, "*"); + pushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + AtomicInteger receivedMsgCount = new AtomicInteger(0); + pushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + receivedMsgCount.addAndGet(msgs.size()); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + pushConsumer.start(); + await().atMost(Duration.ofMinutes(5)).until(() -> receivedMsgCount.get() >= MESSAGE_COUNT); + + await().atMost(Duration.ofMinutes(1)).until(() -> { + pushConsumer.getDefaultMQPushConsumerImpl().persistConsumerOffset(); + Map slaveOffsetTable = null; + for (InnerSalveBrokerController slave : brokerContainer2.getSlaveBrokers()) { + if (slave.getBrokerConfig().getBrokerName().equals(master1With3Replicas.getBrokerConfig().getBrokerName())) { + slaveOffsetTable = slave.getConsumerOffsetManager().queryOffset(CONSUMER_GROUP, TOPIC); + } + } + + if (slaveOffsetTable != null) { + long totalOffset = 0; + for (final Long offset : slaveOffsetTable.values()) { + totalOffset += offset; + } + + return totalOffset >= MESSAGE_COUNT; + } + return false; + }); + + pushConsumer.shutdown(); + cancelIsolatedBroker(master1With3Replicas); + + awaitUntilSlaveOK(); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/container/RebalanceLockOnSlaveIT.java b/test/src/test/java/org/apache/rocketmq/test/container/RebalanceLockOnSlaveIT.java new file mode 100644 index 0000000..2b03301 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/container/RebalanceLockOnSlaveIT.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.test.container; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.impl.FindBrokerResult; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.ServiceState; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.MessageQueue; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +/** + * Test lock on slave when acting master enabled + */ +@Ignore +public class RebalanceLockOnSlaveIT extends ContainerIntegrationTestBase { + private static final String THREE_REPLICA_CONSUMER_GROUP = "SyncConsumerOffsetIT_ConsumerThreeReplica"; + + private static DefaultMQProducer mqProducer; + private static DefaultMQPushConsumer mqConsumerThreeReplica1; + private static DefaultMQPushConsumer mqConsumerThreeReplica2; + private static DefaultMQPushConsumer mqConsumerThreeReplica3; + + public RebalanceLockOnSlaveIT() { + } + + @BeforeClass + public static void beforeClass() throws Exception { + mqProducer = createProducer("SyncConsumerOffsetIT_Producer"); + mqProducer.start(); + + mqConsumerThreeReplica1 = createPushConsumer(THREE_REPLICA_CONSUMER_GROUP); + mqConsumerThreeReplica1.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); + mqConsumerThreeReplica1.subscribe(THREE_REPLICAS_TOPIC, "*"); + + mqConsumerThreeReplica2 = createPushConsumer(THREE_REPLICA_CONSUMER_GROUP); + mqConsumerThreeReplica2.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); + mqConsumerThreeReplica2.subscribe(THREE_REPLICAS_TOPIC, "*"); + + mqConsumerThreeReplica3 = createPushConsumer(THREE_REPLICA_CONSUMER_GROUP); + mqConsumerThreeReplica3.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); + mqConsumerThreeReplica3.subscribe(THREE_REPLICAS_TOPIC, "*"); + } + + @AfterClass + public static void afterClass() { + if (mqProducer != null) { + mqProducer.shutdown(); + } + } + + @Test + public void lockFromSlave() throws Exception { + awaitUntilSlaveOK(); + + mqConsumerThreeReplica3.registerMessageListener((MessageListenerOrderly) (msgs, context) -> ConsumeOrderlyStatus.SUCCESS); + mqConsumerThreeReplica3.start(); + + final Set mqSet = mqConsumerThreeReplica3.fetchSubscribeMessageQueues(THREE_REPLICAS_TOPIC); + + assertThat(targetTopicMqCount(mqSet, THREE_REPLICAS_TOPIC)).isEqualTo(24); + + for (MessageQueue mq : mqSet) { + await().atMost(Duration.ofSeconds(60)).until(() -> mqConsumerThreeReplica3.getDefaultMQPushConsumerImpl().getRebalanceImpl().lock(mq)); + } + + isolateBroker(master3With3Replicas); + + mqConsumerThreeReplica3.getDefaultMQPushConsumerImpl().getmQClientFactory().updateTopicRouteInfoFromNameServer(THREE_REPLICAS_TOPIC); + FindBrokerResult result = mqConsumerThreeReplica3.getDefaultMQPushConsumerImpl().getmQClientFactory().findBrokerAddressInSubscribe( + master3With3Replicas.getBrokerConfig().getBrokerName(), MixAll.MASTER_ID, true); + assertThat(result).isNotNull(); + + for (MessageQueue mq : mqSet) { + if (mq.getBrokerName().equals(master3With3Replicas.getBrokerConfig().getBrokerName())) { + await().atMost(Duration.ofSeconds(60)).until(() -> mqConsumerThreeReplica3.getDefaultMQPushConsumerImpl().getRebalanceImpl().lock(mq)); + } + } + + removeSlaveBroker(1, brokerContainer1, master3With3Replicas); + assertThat(brokerContainer1.getSlaveBrokers().size()).isEqualTo(1); + + mqConsumerThreeReplica3.getDefaultMQPushConsumerImpl().getmQClientFactory().updateTopicRouteInfoFromNameServer(THREE_REPLICAS_TOPIC); + + for (MessageQueue mq : mqSet) { + if (mq.getBrokerName().equals(master3With3Replicas.getBrokerConfig().getBrokerName())) { + await().atMost(Duration.ofSeconds(60)).until(() -> !mqConsumerThreeReplica3.getDefaultMQPushConsumerImpl().getRebalanceImpl().lock(mq)); + } + } + + cancelIsolatedBroker(master3With3Replicas); + createAndAddSlave(1, brokerContainer1, master3With3Replicas); + awaitUntilSlaveOK(); + + mqConsumerThreeReplica3.shutdown(); + await().atMost(100, TimeUnit.SECONDS).until(() -> mqConsumerThreeReplica3.getDefaultMQPushConsumerImpl().getServiceState() == ServiceState.SHUTDOWN_ALREADY); + } + + @Ignore + @Test + public void multiConsumerLockFromSlave() throws MQClientException, InterruptedException { + awaitUntilSlaveOK(); + + mqConsumerThreeReplica1.registerMessageListener((MessageListenerOrderly) (msgs, context) -> ConsumeOrderlyStatus.SUCCESS); + mqConsumerThreeReplica1.start(); + + mqConsumerThreeReplica1.getDefaultMQPushConsumerImpl().doRebalance(); + Set mqSet1 = filterMessageQueue(mqConsumerThreeReplica1.getDefaultMQPushConsumerImpl().getRebalanceImpl().getProcessQueueTable().keySet(), THREE_REPLICAS_TOPIC); + + assertThat(mqSet1.size()).isEqualTo(24); + + isolateBroker(master3With3Replicas); + + System.out.printf("%s isolated%n", master3With3Replicas.getBrokerConfig().getCanonicalName()); + + Thread.sleep(5000); + + mqConsumerThreeReplica2.registerMessageListener((MessageListenerOrderly) (msgs, context) -> ConsumeOrderlyStatus.SUCCESS); + mqConsumerThreeReplica2.start(); + + Thread.sleep(5000); + + mqConsumerThreeReplica1.getDefaultMQPushConsumerImpl().getmQClientFactory().updateTopicRouteInfoFromNameServer(THREE_REPLICAS_TOPIC); + mqConsumerThreeReplica2.getDefaultMQPushConsumerImpl().getmQClientFactory().updateTopicRouteInfoFromNameServer(THREE_REPLICAS_TOPIC); + + assertThat(mqConsumerThreeReplica1.getDefaultMQPushConsumerImpl().getmQClientFactory().findBrokerAddressInSubscribe( + master3With3Replicas.getBrokerConfig().getBrokerName(), MixAll.MASTER_ID, true)).isNotNull(); + + mqConsumerThreeReplica2.getDefaultMQPushConsumerImpl().getmQClientFactory().findBrokerAddressInSubscribe( + master3With3Replicas.getBrokerConfig().getBrokerName(), MixAll.MASTER_ID, true); + assertThat(mqConsumerThreeReplica2.getDefaultMQPushConsumerImpl().getmQClientFactory().findBrokerAddressInSubscribe( + master3With3Replicas.getBrokerConfig().getBrokerName(), MixAll.MASTER_ID, true)).isNotNull(); + + mqConsumerThreeReplica1.getDefaultMQPushConsumerImpl().doRebalance(); + mqConsumerThreeReplica2.getDefaultMQPushConsumerImpl().doRebalance(); + + Set mqSet2 = filterMessageQueue(mqConsumerThreeReplica2.getDefaultMQPushConsumerImpl().getRebalanceImpl().getProcessQueueTable().keySet(), THREE_REPLICAS_TOPIC); + + mqSet1 = filterMessageQueue(mqConsumerThreeReplica1.getDefaultMQPushConsumerImpl().getRebalanceImpl().getProcessQueueTable().keySet(), THREE_REPLICAS_TOPIC); + + List mqList = new ArrayList<>(); + + for (MessageQueue mq : mqSet2) { + if (mq.getTopic().equals(THREE_REPLICAS_TOPIC)) { + mqList.add(mq); + } + } + + for (MessageQueue mq : mqSet1) { + if (mq.getTopic().equals(THREE_REPLICAS_TOPIC)) { + mqList.add(mq); + } + } + + await().atMost(Duration.ofSeconds(30)).until(() -> mqList.size() == 24); + + cancelIsolatedBroker(master3With3Replicas); + awaitUntilSlaveOK(); + + mqConsumerThreeReplica1.shutdown(); + mqConsumerThreeReplica2.shutdown(); + + await().atMost(100, TimeUnit.SECONDS).until(() -> + mqConsumerThreeReplica1.getDefaultMQPushConsumerImpl().getServiceState() == ServiceState.SHUTDOWN_ALREADY && + mqConsumerThreeReplica2.getDefaultMQPushConsumerImpl().getServiceState() == ServiceState.SHUTDOWN_ALREADY + ); + } + + private static int targetTopicMqCount(Set mqSet, String topic) { + int count = 0; + for (MessageQueue mq : mqSet) { + if (mq.getTopic().equals(topic)) { + count++; + } + } + return count; + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/container/ScheduleSlaveActingMasterIT.java b/test/src/test/java/org/apache/rocketmq/test/container/ScheduleSlaveActingMasterIT.java new file mode 100644 index 0000000..aa1ec8f --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/container/ScheduleSlaveActingMasterIT.java @@ -0,0 +1,349 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.test.container; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.commons.lang3.RandomStringUtils; +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.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.BrokerIdentity; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageQueue; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +import static org.awaitility.Awaitility.await; + +//The test is correct, but it takes too much time and not core functions, so it is ignored for the time being +@Ignore +public class ScheduleSlaveActingMasterIT extends ContainerIntegrationTestBase { + + private static final String CONSUME_GROUP = ScheduleSlaveActingMasterIT.class.getSimpleName() + "_Consumer"; + private static final int MESSAGE_COUNT = 32; + private final Random random = new Random(); + private static DefaultMQProducer producer; + private static final String MESSAGE_STRING = RandomStringUtils.random(1024); + private static final byte[] MESSAGE_BODY = MESSAGE_STRING.getBytes(StandardCharsets.UTF_8); + + void createTopic(String topic) { + createTopicTo(master1With3Replicas, topic, 1, 1); + createTopicTo(master2With3Replicas, topic, 1, 1); + createTopicTo(master3With3Replicas, topic, 1, 1); + } + + @BeforeClass + public static void beforeClass() throws Throwable { + producer = createProducer(ScheduleSlaveActingMasterIT.class.getSimpleName() + "_PRODUCER"); + producer.setSendMsgTimeout(5000); + producer.start(); + } + + @AfterClass + public static void afterClass() throws Exception { + producer.shutdown(); + } + + @Test + public void testLocalActing_delayMsg() throws Exception { + awaitUntilSlaveOK(); + String topic = ScheduleSlaveActingMasterIT.class.getSimpleName() + random.nextInt(65535); + createTopic(topic); + DefaultMQPushConsumer pushConsumer = createPushConsumer(CONSUME_GROUP); + pushConsumer.subscribe(topic, "*"); + AtomicInteger receivedMsgCount = new AtomicInteger(0); + AtomicInteger inTimeMsgCount = new AtomicInteger(0); + pushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + long period = System.currentTimeMillis() - msgs.get(0).getBornTimestamp(); + if (Math.abs(period - 30000) <= 4000) { + inTimeMsgCount.addAndGet(msgs.size()); + } + receivedMsgCount.addAndGet(msgs.size()); + msgs.forEach(x -> System.out.printf(x + "%n")); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + pushConsumer.start(); + + MessageQueue messageQueue = new MessageQueue(topic, master1With3Replicas.getBrokerConfig().getBrokerName(), 0); + int sendSuccess = 0; + for (int i = 0; i < MESSAGE_COUNT; i++) { + Message msg = new Message(topic, MESSAGE_BODY); + msg.setDelayTimeLevel(4); + SendResult sendResult = producer.send(msg, messageQueue); + if (sendResult.getSendStatus() == SendStatus.SEND_OK) { + sendSuccess++; + } + } + final int finalSendSuccess = sendSuccess; + await().atMost(Duration.ofMinutes(1)).until(() -> finalSendSuccess >= MESSAGE_COUNT); + System.out.printf("send success%n"); + + isolateBroker(master1With3Replicas); + brokerContainer1.removeBroker(new BrokerIdentity( + master1With3Replicas.getBrokerConfig().getBrokerClusterName(), + master1With3Replicas.getBrokerConfig().getBrokerName(), + master1With3Replicas.getBrokerConfig().getBrokerId())); + + System.out.printf("Remove master1%n"); + + await().atMost(Duration.ofMinutes(1)).until(() -> receivedMsgCount.get() >= MESSAGE_COUNT && inTimeMsgCount.get() >= MESSAGE_COUNT * 0.95); + + System.out.printf("consumer received %d msg, %d in time%n", receivedMsgCount.get(), inTimeMsgCount.get()); + + pushConsumer.shutdown(); + + //Add back master + master1With3Replicas = brokerContainer1.addBroker(master1With3Replicas.getBrokerConfig(), master1With3Replicas.getMessageStoreConfig()); + master1With3Replicas.start(); + cancelIsolatedBroker(master1With3Replicas); + System.out.printf("Add back master1%n"); + + awaitUntilSlaveOK(); + // sleep a while to recover + Thread.sleep(30000); + } + + @Test + public void testLocalActing_timerMsg() throws Exception { + awaitUntilSlaveOK(); + String topic = ScheduleSlaveActingMasterIT.class.getSimpleName() + random.nextInt(65535); + createTopic(topic); + DefaultMQPushConsumer pushConsumer = createPushConsumer(CONSUME_GROUP); + pushConsumer.subscribe(topic, "*"); + AtomicInteger receivedMsgCount = new AtomicInteger(0); + AtomicInteger inTimeMsgCount = new AtomicInteger(0); + pushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + long period = System.currentTimeMillis() - msgs.get(0).getBornTimestamp(); + if (Math.abs(period - 30000) <= 1000) { + inTimeMsgCount.addAndGet(msgs.size()); + } + receivedMsgCount.addAndGet(msgs.size()); + msgs.forEach(x -> System.out.printf(x + "%n")); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + pushConsumer.start(); + + MessageQueue messageQueue = new MessageQueue(topic, master1With3Replicas.getBrokerConfig().getBrokerName(), 0); + int sendSuccess = 0; + for (int i = 0; i < MESSAGE_COUNT; i++) { + Message msg = new Message(topic, MESSAGE_BODY); + msg.setDelayTimeSec(30); + SendResult sendResult = producer.send(msg, messageQueue); + if (sendResult.getSendStatus() == SendStatus.SEND_OK) { + sendSuccess++; + } + } + final int finalSendSuccess = sendSuccess; + await().atMost(Duration.ofMinutes(2)).until(() -> finalSendSuccess >= MESSAGE_COUNT); + System.out.printf("send success%n"); + + isolateBroker(master1With3Replicas); + brokerContainer1.removeBroker(new BrokerIdentity( + master1With3Replicas.getBrokerConfig().getBrokerClusterName(), + master1With3Replicas.getBrokerConfig().getBrokerName(), + master1With3Replicas.getBrokerConfig().getBrokerId())); + + System.out.printf("Remove master1%n"); + + await().atMost(Duration.ofMinutes(1)).until(() -> receivedMsgCount.get() >= MESSAGE_COUNT && inTimeMsgCount.get() >= MESSAGE_COUNT * 0.95); + + System.out.printf("consumer received %d msg, %d in time%n", receivedMsgCount.get(), inTimeMsgCount.get()); + + pushConsumer.shutdown(); + + //Add back master + master1With3Replicas = brokerContainer1.addBroker(master1With3Replicas.getBrokerConfig(), master1With3Replicas.getMessageStoreConfig()); + master1With3Replicas.start(); + cancelIsolatedBroker(master1With3Replicas); + System.out.printf("Add back master1%n"); + + awaitUntilSlaveOK(); + // sleep a while to recover + Thread.sleep(20000); + } + + @Test + public void testRemoteActing_delayMsg() throws Exception { + awaitUntilSlaveOK(); + + String topic = ScheduleSlaveActingMasterIT.class.getSimpleName() + random.nextInt(65535); + createTopic(topic); + AtomicInteger receivedMsgCount = new AtomicInteger(0); + AtomicInteger inTimeMsgCount = new AtomicInteger(0); + AtomicInteger master3MsgCount = new AtomicInteger(0); + + MessageQueue messageQueue = new MessageQueue(topic, master1With3Replicas.getBrokerConfig().getBrokerName(), 0); + int sendSuccess = 0; + for (int i = 0; i < MESSAGE_COUNT; i++) { + Message msg = new Message(topic, MESSAGE_BODY); + msg.setDelayTimeLevel(4); + SendResult sendResult = producer.send(msg, messageQueue); + if (sendResult.getSendStatus() == SendStatus.SEND_OK) { + sendSuccess++; + } + } + final int finalSendSuccess = sendSuccess; + await().atMost(Duration.ofMinutes(1)).until(() -> finalSendSuccess >= MESSAGE_COUNT); + long sendCompleteTimeStamp = System.currentTimeMillis(); + System.out.printf("send success%n"); + + DefaultMQPushConsumer pushConsumer = createPushConsumer(CONSUME_GROUP); + pushConsumer.subscribe(topic, "*"); + pushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + long period = System.currentTimeMillis() - sendCompleteTimeStamp; + // Remote Acting lead to born timestamp, msgId changed, it need to polish. + if (Math.abs(period - 30000) <= 4000) { + inTimeMsgCount.addAndGet(msgs.size()); + } + if (msgs.get(0).getBrokerName().equals(master3With3Replicas.getBrokerConfig().getBrokerName())) { + master3MsgCount.addAndGet(msgs.size()); + } + receivedMsgCount.addAndGet(msgs.size()); + msgs.forEach(x -> System.out.printf("cost " + period + " " + x + "%n")); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + pushConsumer.start(); + + isolateBroker(master1With3Replicas); + BrokerIdentity master1BrokerIdentity = new BrokerIdentity( + master1With3Replicas.getBrokerConfig().getBrokerClusterName(), + master1With3Replicas.getBrokerConfig().getBrokerName(), + master1With3Replicas.getBrokerConfig().getBrokerId()); + + brokerContainer1.removeBroker(master1BrokerIdentity); + System.out.printf("Remove master1%n"); + + isolateBroker(master2With3Replicas); + BrokerIdentity master2BrokerIdentity = new BrokerIdentity( + master2With3Replicas.getBrokerConfig().getBrokerClusterName(), + master2With3Replicas.getBrokerConfig().getBrokerName(), + master2With3Replicas.getBrokerConfig().getBrokerId()); + brokerContainer2.removeBroker(master2BrokerIdentity); + System.out.printf("Remove master2%n"); + + await().atMost(Duration.ofMinutes(2)).until(() -> receivedMsgCount.get() >= MESSAGE_COUNT && master3MsgCount.get() >= MESSAGE_COUNT && inTimeMsgCount.get() >= MESSAGE_COUNT * 0.95); + + System.out.printf("consumer received %d msg, %d in time%n", receivedMsgCount.get(), inTimeMsgCount.get()); + + pushConsumer.shutdown(); + + //Add back master + master1With3Replicas = brokerContainer1.addBroker(master1With3Replicas.getBrokerConfig(), master1With3Replicas.getMessageStoreConfig()); + master1With3Replicas.start(); + cancelIsolatedBroker(master1With3Replicas); + System.out.printf("Add back master1%n"); + + //Add back master + master2With3Replicas = brokerContainer2.addBroker(master2With3Replicas.getBrokerConfig(), master2With3Replicas.getMessageStoreConfig()); + master2With3Replicas.start(); + cancelIsolatedBroker(master2With3Replicas); + System.out.printf("Add back master2%n"); + + awaitUntilSlaveOK(); + // sleep a while to recover + Thread.sleep(30000); + } + + @Test + public void testRemoteActing_timerMsg() throws Exception { + awaitUntilSlaveOK(); + + String topic = ScheduleSlaveActingMasterIT.class.getSimpleName() + random.nextInt(65535); + createTopic(topic); + AtomicInteger receivedMsgCount = new AtomicInteger(0); + AtomicInteger inTimeMsgCount = new AtomicInteger(0); + AtomicInteger master3MsgCount = new AtomicInteger(0); + + MessageQueue messageQueue = new MessageQueue(topic, master1With3Replicas.getBrokerConfig().getBrokerName(), 0); + int sendSuccess = 0; + for (int i = 0; i < MESSAGE_COUNT; i++) { + Message msg = new Message(topic, MESSAGE_BODY); + msg.setDelayTimeSec(30); + SendResult sendResult = producer.send(msg, messageQueue); + if (sendResult.getSendStatus() == SendStatus.SEND_OK) { + sendSuccess++; + } + } + final int finalSendSuccess = sendSuccess; + await().atMost(Duration.ofMinutes(1)).until(() -> finalSendSuccess >= MESSAGE_COUNT); + long sendCompleteTimeStamp = System.currentTimeMillis(); + System.out.printf("send success%n"); + + DefaultMQPushConsumer pushConsumer = createPushConsumer(CONSUME_GROUP); + pushConsumer.subscribe(topic, "*"); + pushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + long period = System.currentTimeMillis() - sendCompleteTimeStamp; + // Remote Acting lead to born timestamp, msgId changed, it need to polish. + if (Math.abs(period - 30000) <= 3000) { + inTimeMsgCount.addAndGet(msgs.size()); + } + if (msgs.get(0).getBrokerName().equals(master3With3Replicas.getBrokerConfig().getBrokerName())) { + master3MsgCount.addAndGet(msgs.size()); + } + receivedMsgCount.addAndGet(msgs.size()); + msgs.forEach(x -> System.out.printf("cost " + period + " " + x + "%n")); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + pushConsumer.start(); + + isolateBroker(master1With3Replicas); + brokerContainer1.removeBroker(new BrokerIdentity( + master1With3Replicas.getBrokerConfig().getBrokerClusterName(), + master1With3Replicas.getBrokerConfig().getBrokerName(), + master1With3Replicas.getBrokerConfig().getBrokerId())); + System.out.printf("Remove master1%n"); + + isolateBroker(master2With3Replicas); + brokerContainer2.removeBroker(new BrokerIdentity( + master2With3Replicas.getBrokerConfig().getBrokerClusterName(), + master2With3Replicas.getBrokerConfig().getBrokerName(), + master2With3Replicas.getBrokerConfig().getBrokerId())); + System.out.printf("Remove master2%n"); + + await().atMost(Duration.ofMinutes(1)).until(() -> receivedMsgCount.get() >= MESSAGE_COUNT && master3MsgCount.get() >= MESSAGE_COUNT && inTimeMsgCount.get() >= MESSAGE_COUNT * 0.95); + + System.out.printf("consumer received %d msg, %d in time%n", receivedMsgCount.get(), inTimeMsgCount.get()); + + pushConsumer.shutdown(); + + //Add back master + master1With3Replicas = brokerContainer1.addBroker(master1With3Replicas.getBrokerConfig(), master1With3Replicas.getMessageStoreConfig()); + master1With3Replicas.start(); + cancelIsolatedBroker(master1With3Replicas); + System.out.printf("Add back master1%n"); + + //Add back master + master2With3Replicas = brokerContainer2.addBroker(master2With3Replicas.getBrokerConfig(), master2With3Replicas.getMessageStoreConfig()); + master2With3Replicas.start(); + cancelIsolatedBroker(master2With3Replicas); + System.out.printf("Add back master2%n"); + + awaitUntilSlaveOK(); + // sleep a while to recover + Thread.sleep(20000); + } + +} diff --git a/test/src/test/java/org/apache/rocketmq/test/container/ScheduledMessageIT.java b/test/src/test/java/org/apache/rocketmq/test/container/ScheduledMessageIT.java new file mode 100644 index 0000000..0c4b8c3 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/container/ScheduledMessageIT.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.test.container; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.commons.lang3.RandomStringUtils; +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.MQBrokerException; +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.exception.RemotingException; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@Ignore +public class ScheduledMessageIT extends ContainerIntegrationTestBase { + private static DefaultMQProducer producer; + + private static final String CONSUME_GROUP = ScheduledMessageIT.class.getSimpleName() + "_Consumer"; + private static final String MESSAGE_STRING = RandomStringUtils.random(1024); + private static final byte[] MESSAGE_BODY = MESSAGE_STRING.getBytes(StandardCharsets.UTF_8); + + private static final String TOPIC_PREFIX = ScheduledMessageIT.class.getSimpleName() + "_TOPIC"; + private final Random random = new Random(); + private static final int MESSAGE_COUNT = 128; + + public ScheduledMessageIT() throws UnsupportedEncodingException { + } + + void createTopic(String topic) { + createTopicTo(master1With3Replicas, topic, 1, 1); + createTopicTo(master2With3Replicas, topic, 1, 1); + createTopicTo(master3With3Replicas, topic, 1, 1); + } + + @BeforeClass + public static void beforeClass() throws Throwable { + producer = createProducer(ScheduledMessageIT.class.getSimpleName() + "_PRODUCER"); + producer.setSendMsgTimeout(5000); + producer.start(); + } + + @AfterClass + public static void afterClass() throws Exception { + producer.shutdown(); + } + + @Ignore + @Test + public void consumeScheduledMsg() throws MQClientException, RemotingException, InterruptedException, MQBrokerException { + String topic = TOPIC_PREFIX + random.nextInt(65535); + createTopic(topic); + DefaultMQPushConsumer pushConsumer = createPushConsumer(CONSUME_GROUP + random.nextInt(65535)); + pushConsumer.subscribe(topic, "*"); + AtomicInteger receivedMsgCount = new AtomicInteger(0); + AtomicInteger inTimeMsgCount = new AtomicInteger(0); + pushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + long period = System.currentTimeMillis() - msgs.get(0).getBornTimestamp(); + if (Math.abs(period - 5000) <= 1000) { + inTimeMsgCount.addAndGet(msgs.size()); + } + receivedMsgCount.addAndGet(msgs.size()); + msgs.forEach(x -> System.out.printf(receivedMsgCount.get() + " cost " + period + " " + x + "%n")); + + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + pushConsumer.start(); + + for (int i = 0; i < MESSAGE_COUNT; i++) { + Message msg = new Message(topic, MESSAGE_BODY); + msg.setDelayTimeLevel(2); + producer.send(msg); + } + + await().atMost(Duration.ofSeconds(MESSAGE_COUNT * 2)).until(() -> receivedMsgCount.get() >= MESSAGE_COUNT && inTimeMsgCount.get() >= MESSAGE_COUNT * 0.95); + + System.out.printf("consumer received %d msg, %d in time%n", receivedMsgCount.get(), inTimeMsgCount.get()); + + pushConsumer.shutdown(); + } + + @Test + public void consumeScheduledMsgFromSlave() throws MQClientException, RemotingException, InterruptedException, MQBrokerException { + String topic = TOPIC_PREFIX + random.nextInt(65535); + createTopic(topic); + DefaultMQPushConsumer pushConsumer = createPushConsumer(CONSUME_GROUP + random.nextInt(65535)); + pushConsumer.subscribe(topic, "*"); + AtomicInteger receivedMsgCount = new AtomicInteger(0); + pushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + receivedMsgCount.addAndGet(msgs.size()); + msgs.forEach(x -> System.out.printf(x + "%n")); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + + for (int i = 0; i < MESSAGE_COUNT; i++) { + Message msg = new Message(topic, String.valueOf(i).getBytes()); + msg.setDelayTimeLevel(2); + producer.send(msg); + } + + isolateBroker(master1With3Replicas); + + producer.getDefaultMQProducerImpl().getmQClientFactory().updateTopicRouteInfoFromNameServer(topic); + assertThat(producer.getDefaultMQProducerImpl().getmQClientFactory().findBrokerAddressInPublish(topic)).isNull(); + + pushConsumer.start(); + + await().atMost(Duration.ofSeconds(MESSAGE_COUNT * 2)).until(() -> receivedMsgCount.get() >= MESSAGE_COUNT); + + pushConsumer.shutdown(); + cancelIsolatedBroker(master1With3Replicas); + + await().atMost(100, TimeUnit.SECONDS) + .until(() -> ((DefaultMessageStore) master1With3Replicas.getMessageStore()).getHaService().getConnectionCount().get() == 2); + } + + @Test + public void consumeTimerMsgFromSlave() throws MQClientException, RemotingException, InterruptedException, MQBrokerException { + String topic = TOPIC_PREFIX + random.nextInt(65535); + createTopic(topic); + DefaultMQPushConsumer pushConsumer = createPushConsumer(CONSUME_GROUP); + pushConsumer.subscribe(topic, "*"); + AtomicInteger receivedMsgCount = new AtomicInteger(0); + pushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + receivedMsgCount.addAndGet(msgs.size()); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + + for (int i = 0; i < MESSAGE_COUNT; i++) { + Message msg = new Message(topic, String.valueOf(i).getBytes()); + msg.setDelayTimeSec(3); + producer.send(msg); + } + + isolateBroker(master1With3Replicas); + + producer.getDefaultMQProducerImpl().getmQClientFactory().updateTopicRouteInfoFromNameServer(topic); + assertThat(producer.getDefaultMQProducerImpl().getmQClientFactory().findBrokerAddressInPublish(topic)).isNull(); + + pushConsumer.start(); + + await().atMost(Duration.ofSeconds(MESSAGE_COUNT * 2)).until(() -> receivedMsgCount.get() >= MESSAGE_COUNT); + + pushConsumer.shutdown(); + cancelIsolatedBroker(master1With3Replicas); + + await().atMost(100, TimeUnit.SECONDS) + .until(() -> ((DefaultMessageStore) master1With3Replicas.getMessageStore()).getHaService().getConnectionCount().get() == 2); + } + +} diff --git a/test/src/test/java/org/apache/rocketmq/test/container/SendMultipleReplicasIT.java b/test/src/test/java/org/apache/rocketmq/test/container/SendMultipleReplicasIT.java new file mode 100644 index 0000000..76df1f7 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/container/SendMultipleReplicasIT.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.test.container; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.TimeUnit; +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.common.message.MessageQueue; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@Ignore +public class SendMultipleReplicasIT extends ContainerIntegrationTestBase { + private static DefaultMQProducer mqProducer; + private static final String MSG = "Hello RocketMQ "; + private static final byte[] MESSAGE_BODY = MSG.getBytes(StandardCharsets.UTF_8); + + public SendMultipleReplicasIT() { + } + + @BeforeClass + public static void beforeClass() throws Exception { + mqProducer = createProducer("SendMultipleReplicasMessageIT_Producer"); + mqProducer.setSendMsgTimeout(15 * 1000); + mqProducer.start(); + } + + @AfterClass + public static void afterClass() { + if (mqProducer != null) { + mqProducer.shutdown(); + } + } + + @Test + public void sendMessageToBrokerGroup() throws MQClientException, RemotingException, InterruptedException, MQBrokerException { + awaitUntilSlaveOK(); + + // Send message to broker group with three replicas + Message msg = new Message(THREE_REPLICAS_TOPIC, MESSAGE_BODY); + SendResult sendResult = mqProducer.send(msg); + assertThat(sendResult.getSendStatus()).isEqualTo(SendStatus.SEND_OK); + } + + @Test + public void sendMessage_Auto_Replicas_Success() throws Exception { + await().atMost(100, TimeUnit.SECONDS) + .until(() -> ((DefaultMessageStore) master1With3Replicas.getMessageStore()).getHaService().getConnectionCount().get() == 2 + && master1With3Replicas.getMessageStore().getAliveReplicaNumInGroup() == 3); + // Broker with 3 replicas configured as 3-2-1 auto replicas mode + Message msg = new Message(THREE_REPLICAS_TOPIC, MESSAGE_BODY); + SendResult sendResult = mqProducer.send(msg); + assertThat(sendResult.getSendStatus()).isEqualTo(SendStatus.SEND_OK); + + // Remove two slave broker + removeSlaveBroker(1, brokerContainer2, master1With3Replicas); + removeSlaveBroker(2, brokerContainer3, master1With3Replicas); + await().atMost(100, TimeUnit.SECONDS) + .until(() -> + ((DefaultMessageStore) master1With3Replicas.getMessageStore()).getHaService().getConnectionCount().get() == 0 + && master1With3Replicas.getMessageStore().getAliveReplicaNumInGroup() == 1); + + master1With3Replicas.getMessageStoreConfig().setEnableAutoInSyncReplicas(true); + List mqList = mqProducer.getDefaultMQProducerImpl().fetchPublishMessageQueues(THREE_REPLICAS_TOPIC); + MessageQueue targetMq = null; + for (MessageQueue mq : mqList) { + if (mq.getBrokerName().equals(master1With3Replicas.getBrokerConfig().getBrokerName())) { + targetMq = mq; + } + } + + assertThat(targetMq).isNotNull(); + // Although this broker group only has one slave broker, send will be success in auto mode. + msg = new Message(THREE_REPLICAS_TOPIC, MESSAGE_BODY); + sendResult = mqProducer.send(msg, targetMq); + assertThat(sendResult.getSendStatus()).isEqualTo(SendStatus.SEND_OK); + + // Recover the cluster state + createAndAddSlave(1, brokerContainer2, master1With3Replicas); + createAndAddSlave(2, brokerContainer3, master1With3Replicas); + await().atMost(100, TimeUnit.SECONDS) + .until(() -> ((DefaultMessageStore) master1With3Replicas.getMessageStore()).getHaService().getConnectionCount().get() == 2 + && master1With3Replicas.getMessageStore().getAliveReplicaNumInGroup() == 3); + } + + @Test + public void sendMessage_Auto_Replicas_Failed() + throws Exception { + await().atMost(100, TimeUnit.SECONDS) + .until(() -> ((DefaultMessageStore) master1With3Replicas.getMessageStore()).getHaService().getConnectionCount().get() == 2 + && master1With3Replicas.getMessageStore().getAliveReplicaNumInGroup() == 3); + // Broker with 3 replicas configured as 3-2-1 auto replicas mode + // Remove two slave broker + removeSlaveBroker(1, brokerContainer2, master1With3Replicas); + removeSlaveBroker(2, brokerContainer3, master1With3Replicas); + await().atMost(100, TimeUnit.SECONDS) + .until(() -> + ((DefaultMessageStore) master1With3Replicas.getMessageStore()).getHaService().getConnectionCount().get() == 0 + && master1With3Replicas.getMessageStore().getAliveReplicaNumInGroup() == 1); + + // Disable the auto mode + master1With3Replicas.getMessageStoreConfig().setEnableAutoInSyncReplicas(false); + + List mqList = mqProducer.getDefaultMQProducerImpl().fetchPublishMessageQueues(THREE_REPLICAS_TOPIC); + MessageQueue targetMq = null; + for (MessageQueue mq : mqList) { + if (mq.getBrokerName().equals(master1With3Replicas.getBrokerConfig().getBrokerName())) { + targetMq = mq; + } + } + + assertThat(targetMq).isNotNull(); + + Message msg = new Message(THREE_REPLICAS_TOPIC, MESSAGE_BODY); + boolean exceptionCaught = false; + try { + mqProducer.send(msg, targetMq); + } catch (MQBrokerException e) { + exceptionCaught = true; + } + + assertThat(exceptionCaught).isTrue(); + // Recover the cluster state + createAndAddSlave(1, brokerContainer2, master1With3Replicas); + createAndAddSlave(2, brokerContainer3, master1With3Replicas); + await().atMost(100, TimeUnit.SECONDS) + .until(() -> ((DefaultMessageStore) master1With3Replicas.getMessageStore()).getHaService().getConnectionCount().get() == 2 + && master1With3Replicas.getMessageStore().getAliveReplicaNumInGroup() == 3); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/container/SlaveBrokerIT.java b/test/src/test/java/org/apache/rocketmq/test/container/SlaveBrokerIT.java new file mode 100644 index 0000000..30dc046 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/container/SlaveBrokerIT.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.test.container; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.junit.Ignore; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@Ignore +public class SlaveBrokerIT extends ContainerIntegrationTestBase { + @Test + public void reAddSlaveBroker() throws Exception { + await().atMost(Duration.ofMinutes(1)).until(() -> { + ClusterInfo clusterInfo = defaultMQAdminExt.examineBrokerClusterInfo(); + + if (clusterInfo.getClusterAddrTable().get(master1With3Replicas.getBrokerConfig().getBrokerClusterName()).size() != 3) { + return false; + } + + if (clusterInfo.getBrokerAddrTable().get(master1With3Replicas.getBrokerConfig().getBrokerName()).getBrokerAddrs().size() != 3) { + return false; + } + + if (clusterInfo.getBrokerAddrTable().get(master2With3Replicas.getBrokerConfig().getBrokerName()).getBrokerAddrs().size() != 3) { + return false; + } + + if (clusterInfo.getBrokerAddrTable().get(master3With3Replicas.getBrokerConfig().getBrokerName()).getBrokerAddrs().size() != 3) { + return false; + } + + return true; + }); + + // Remove one replicas from each broker group + removeSlaveBroker(1, brokerContainer1, master3With3Replicas); + removeSlaveBroker(1, brokerContainer2, master1With3Replicas); + removeSlaveBroker(1, brokerContainer3, master2With3Replicas); + + await().atMost(Duration.ofMinutes(1)).until(() -> { + // Test cluster info again + ClusterInfo clusterInfo = defaultMQAdminExt.examineBrokerClusterInfo(); + assertThat(clusterInfo.getBrokerAddrTable().get(master1With3Replicas.getBrokerConfig().getBrokerName()).getBrokerAddrs().size()) + .isEqualTo(2); + + assertThat(clusterInfo.getBrokerAddrTable().get(master2With3Replicas.getBrokerConfig().getBrokerName()).getBrokerAddrs().size()) + .isEqualTo(2); + + assertThat(clusterInfo.getBrokerAddrTable().get(master3With3Replicas.getBrokerConfig().getBrokerName()).getBrokerAddrs().size()) + .isEqualTo(2); + return true; + }); + + // ReAdd the slave broker + createAndAddSlave(1, brokerContainer1, master3With3Replicas); + createAndAddSlave(1, brokerContainer2, master1With3Replicas); + createAndAddSlave(1, brokerContainer3, master2With3Replicas); + + // Trigger a register action + //for (final SlaveBrokerController slaveBrokerController : brokerContainer1.getSlaveBrokers()) { + // slaveBrokerController.registerBrokerAll(false, false, true); + //} + // + //for (final SlaveBrokerController slaveBrokerController : brokerContainer2.getSlaveBrokers()) { + // slaveBrokerController.registerBrokerAll(false, false, true); + //} + + await().atMost(Duration.ofMinutes(1)).until(() -> { + ClusterInfo clusterInfo = defaultMQAdminExt.examineBrokerClusterInfo(); + + return clusterInfo.getBrokerAddrTable() + .get(master1With3Replicas.getBrokerConfig().getBrokerName()).getBrokerAddrs().size() == 3 + && clusterInfo.getBrokerAddrTable() + .get(master2With3Replicas.getBrokerConfig().getBrokerName()).getBrokerAddrs().size() == 3 + && clusterInfo.getBrokerAddrTable() + .get(master2With3Replicas.getBrokerConfig().getBrokerName()).getBrokerAddrs().size() == 3; + }); + } + + @Test + public void reAddSlaveBroker_ConnectionCheck() throws Exception { + await().atMost(100, TimeUnit.SECONDS) + .until(() -> ((DefaultMessageStore) master3With3Replicas.getMessageStore()).getHaService().getConnectionCount().get() == 2); + + removeSlaveBroker(1, brokerContainer1, master3With3Replicas); + createAndAddSlave(1, brokerContainer1, master3With3Replicas); + + await().atMost(100, TimeUnit.SECONDS) + .until(() -> ((DefaultMessageStore) master3With3Replicas.getMessageStore()).getHaService().getConnectionCount().get() == 2); + + await().atMost(100, TimeUnit.SECONDS) + .until(() -> ((DefaultMessageStore) master3With3Replicas.getMessageStore()).getHaService().inSyncReplicasNums(0) == 3); + + Thread.sleep(1000 * 101); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/container/SyncConsumerOffsetIT.java b/test/src/test/java/org/apache/rocketmq/test/container/SyncConsumerOffsetIT.java new file mode 100644 index 0000000..5a9ac71 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/container/SyncConsumerOffsetIT.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.test.container; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.container.BrokerContainer; +import org.apache.rocketmq.container.InnerSalveBrokerController; +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.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.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@Ignore +public class SyncConsumerOffsetIT extends ContainerIntegrationTestBase { + private static final String THREE_REPLICA_CONSUMER_GROUP = "SyncConsumerOffsetIT_ConsumerThreeReplica"; + private static final String TEST_SYNC_TOPIC = SyncConsumerOffsetIT.class.getSimpleName() + "_topic"; + + private static DefaultMQProducer mqProducer; + private static DefaultMQPushConsumer mqConsumerThreeReplica; + private static final String MSG = "Hello RocketMQ "; + private static final byte[] MESSAGE_BODY = MSG.getBytes(StandardCharsets.UTF_8); + + public SyncConsumerOffsetIT() { + } + + @BeforeClass + public static void beforeClass() throws Exception { + createTopicTo(master3With3Replicas, TEST_SYNC_TOPIC); + + mqProducer = createProducer("SyncConsumerOffsetIT_Producer"); + mqProducer.setSendMsgTimeout(15 * 1000); + mqProducer.start(); + + mqConsumerThreeReplica = createPushConsumer(THREE_REPLICA_CONSUMER_GROUP); + mqConsumerThreeReplica.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); + mqConsumerThreeReplica.subscribe(TEST_SYNC_TOPIC, "*"); + } + + @AfterClass + public static void afterClass() { + mqProducer.shutdown(); + mqConsumerThreeReplica.shutdown(); + } + + @Test + public void syncConsumerOffsetWith3Replicas() throws InterruptedException, RemotingException, MQClientException, MQBrokerException { + syncConsumeOffsetInner(TEST_SYNC_TOPIC, mqConsumerThreeReplica, + master3With3Replicas, Arrays.asList(brokerContainer1, brokerContainer2)); + } + + private void syncConsumeOffsetInner(String topic, DefaultMQPushConsumer consumer, BrokerController master, + List slaveContainers) throws InterruptedException, RemotingException, MQClientException, MQBrokerException { + awaitUntilSlaveOK(); + String group = THREE_REPLICA_CONSUMER_GROUP; + + int msgCount = 100; + for (int i = 0; i < msgCount; i++) { + Message msg = new Message(topic, MESSAGE_BODY); + SendResult sendResult = mqProducer.send(msg); + assertThat(sendResult.getSendStatus()).isEqualTo(SendStatus.SEND_OK); + } + + CountDownLatch countDownLatch = new CountDownLatch(msgCount); + consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + countDownLatch.countDown(); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + consumer.start(); + boolean ok = countDownLatch.await(100, TimeUnit.SECONDS); + assertThat(ok).isEqualTo(true); + System.out.printf("consume complete%n"); + + final Set mqSet = filterMessageQueue(consumer.fetchSubscribeMessageQueues(topic), topic); + + await().atMost(120, TimeUnit.SECONDS).until(() -> { + Map consumerOffsetMap = new HashMap<>(); + long offsetTotal = 0L; + for (MessageQueue mq : mqSet) { + long queueOffset = master.getConsumerOffsetManager().queryOffset(group, topic, mq.getQueueId()); + if (queueOffset < 0) { + continue; + } + offsetTotal += queueOffset; + consumerOffsetMap.put(mq.getQueueId(), queueOffset); + } + + if (offsetTotal < 100) { + return false; + } + boolean syncOk = true; + + for (BrokerContainer brokerContainer : slaveContainers) { + for (InnerSalveBrokerController slave : brokerContainer.getSlaveBrokers()) { + if (!slave.getBrokerConfig().getBrokerName().equals(master.getBrokerConfig().getBrokerName())) { + continue; + } + for (MessageQueue mq : mqSet) { + long slaveOffset = slave.getConsumerOffsetManager().queryOffset(group, topic, mq.getQueueId()); + boolean check = slaveOffset == consumerOffsetMap.get(mq.getQueueId()); + syncOk &= check; + } + } + } + + return syncOk; + }); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/container/TransactionListenerImpl.java b/test/src/test/java/org/apache/rocketmq/test/container/TransactionListenerImpl.java new file mode 100644 index 0000000..177d91e --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/container/TransactionListenerImpl.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.test.container; + +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; + +public class TransactionListenerImpl implements TransactionListener { + private boolean shouldReturnUnknownState = false; + + + + public TransactionListenerImpl(boolean shouldReturnUnknownState) { + this.shouldReturnUnknownState = shouldReturnUnknownState; + } + + public void setShouldReturnUnknownState(boolean shouldReturnUnknownState) { + this.shouldReturnUnknownState = shouldReturnUnknownState; + } + + @Override + public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { + if (shouldReturnUnknownState) { + return LocalTransactionState.UNKNOW; + } else { + return LocalTransactionState.COMMIT_MESSAGE; + } + } + + @Override + public LocalTransactionState checkLocalTransaction(MessageExt msg) { + if (shouldReturnUnknownState) { + return LocalTransactionState.UNKNOW; + } else { + return LocalTransactionState.COMMIT_MESSAGE; + } + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/container/TransactionMessageIT.java b/test/src/test/java/org/apache/rocketmq/test/container/TransactionMessageIT.java new file mode 100644 index 0000000..e2e020d --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/container/TransactionMessageIT.java @@ -0,0 +1,277 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.test.container; + +import java.io.UnsupportedEncodingException; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.commons.lang3.RandomStringUtils; +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.producer.LocalTransactionState; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.TransactionMQProducer; +import org.apache.rocketmq.client.producer.TransactionSendResult; +import org.apache.rocketmq.common.BrokerIdentity; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.junit.Ignore; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@Ignore +public class TransactionMessageIT extends ContainerIntegrationTestBase { + + private static final String MESSAGE_STRING = RandomStringUtils.random(1024); + private static byte[] messageBody; + + static { + try { + messageBody = MESSAGE_STRING.getBytes(RemotingHelper.DEFAULT_CHARSET); + } catch (UnsupportedEncodingException ignored) { + } + } + + private static final int MESSAGE_COUNT = 16; + + public TransactionMessageIT() { + } + + private static String generateGroup() { + return "GID-" + TransactionMessageIT.class.getSimpleName() + RandomStringUtils.randomNumeric(5); + } + + @Test + public void consumeTransactionMsg() throws MQClientException { + final String topic = generateTopic(); + createTopicTo(master1With3Replicas, topic, 1, 1); + + final String group = generateGroup(); + DefaultMQPushConsumer pushConsumer = createPushConsumer(group); + pushConsumer.subscribe(topic, "*"); + AtomicInteger receivedMsgCount = new AtomicInteger(0); + pushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + receivedMsgCount.addAndGet(msgs.size()); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + pushConsumer.start(); + + TransactionMQProducer producer = createTransactionProducer(group, new TransactionListenerImpl(false)); + producer.start(); + + for (int i = 0; i < MESSAGE_COUNT; i++) { + Message msg = new Message(topic, messageBody); + TransactionSendResult result = producer.sendMessageInTransaction(msg, null); + assertThat(result.getLocalTransactionState()).isEqualTo(LocalTransactionState.COMMIT_MESSAGE); + } + + System.out.printf("send message complete%n"); + + await().atMost(Duration.ofSeconds(MESSAGE_COUNT * 2)).until(() -> receivedMsgCount.get() >= MESSAGE_COUNT); + + System.out.printf("consumer received %d msg%n", receivedMsgCount.get()); + + pushConsumer.shutdown(); + producer.shutdown(); + } + + private static String generateTopic() { + return TransactionMessageIT.class.getSimpleName() + RandomStringUtils.randomNumeric(5); + } + + @Test + public void consumeTransactionMsgLocalEscape() throws Exception { + final String topic = generateTopic(); + createTopicTo(master1With3Replicas, topic, 1, 1); + + final String group = generateGroup(); + DefaultMQPushConsumer pushConsumer = createPushConsumer(group); + pushConsumer.subscribe(topic, "*"); + AtomicInteger receivedMsgCount = new AtomicInteger(0); + Map msgSentMap = new HashMap<>(); + pushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + for (MessageExt msg : msgs) { + if (msgSentMap.containsKey(msg.getMsgId())) { + receivedMsgCount.incrementAndGet(); + } + } + + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + pushConsumer.start(); + + TransactionListenerImpl transactionCheckListener = new TransactionListenerImpl(true); + TransactionMQProducer producer = createTransactionProducer(group, transactionCheckListener); + producer.start(); + + for (int i = 0; i < MESSAGE_COUNT; i++) { + Message msg = new Message(topic, messageBody); + msg.setKeys(UUID.randomUUID().toString()); + SendResult result = producer.sendMessageInTransaction(msg, null); + String msgId = result.getMsgId(); + + msgSentMap.put(msgId, msg); + } + + isolateBroker(master1With3Replicas); + brokerContainer1.removeBroker(new BrokerIdentity(master1With3Replicas.getBrokerIdentity().getBrokerClusterName(), + master1With3Replicas.getBrokerIdentity().getBrokerName(), + master1With3Replicas.getBrokerIdentity().getBrokerId())); + System.out.printf("=========" + master1With3Replicas.getBrokerIdentity().getBrokerName() + "-" + + master1With3Replicas.getBrokerIdentity().getBrokerId() + " removed%n"); + createTopicTo(master2With3Replicas, topic, 1, 1); + + transactionCheckListener.setShouldReturnUnknownState(false); + producer.getDefaultMQProducerImpl().getmQClientFactory().updateTopicRouteInfoFromNameServer(topic); + + System.out.printf("Wait for consuming%n"); + + await().atMost(Duration.ofSeconds(300)).until(() -> receivedMsgCount.get() >= MESSAGE_COUNT); + + System.out.printf("consumer received %d msg%n", receivedMsgCount.get()); + + pushConsumer.shutdown(); + producer.shutdown(); + + master1With3Replicas = brokerContainer1.addBroker(master1With3Replicas.getBrokerConfig(), master1With3Replicas.getMessageStoreConfig()); + master1With3Replicas.start(); + cancelIsolatedBroker(master1With3Replicas); + awaitUntilSlaveOK(); + + receivedMsgCount.set(0); + DefaultMQPushConsumer pushConsumer2 = createPushConsumer(group); + pushConsumer2.subscribe(topic, "*"); + pushConsumer2.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + for (MessageExt msg : msgs) { + if (msgSentMap.containsKey(msg.getMsgId())) { + receivedMsgCount.incrementAndGet(); + } + } + + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + pushConsumer2.start(); + System.out.printf("Wait for checking...%n"); + Thread.sleep(10000L); + + } + + @Test + public void consumeTransactionMsgRemoteEscape() throws Exception { + final String topic = generateTopic(); + createTopicTo(master1With3Replicas, topic, 1, 1); + + final String group = generateGroup(); + + AtomicInteger receivedMsgCount = new AtomicInteger(0); + Map msgSentMap = new HashMap<>(); + DefaultMQPushConsumer pushConsumer = createPushConsumer(group); + pushConsumer.subscribe(topic, "*"); + pushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + for (MessageExt msg : msgs) { + if (msgSentMap.containsKey(msg.getMsgId())) { + receivedMsgCount.incrementAndGet(); + } + } + + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + pushConsumer.start(); + + TransactionListenerImpl transactionCheckListener = new TransactionListenerImpl(true); + TransactionMQProducer producer = createTransactionProducer(group, transactionCheckListener); + producer.start(); + + for (int i = 0; i < MESSAGE_COUNT; i++) { + Message msg = new Message(topic, messageBody); + msg.setKeys(UUID.randomUUID().toString()); + SendResult result = producer.sendMessageInTransaction(msg, null); + String msgId = result.getMsgId(); + + msgSentMap.put(msgId, msg); + } + + isolateBroker(master1With3Replicas); + brokerContainer1.removeBroker(new BrokerIdentity(master1With3Replicas.getBrokerIdentity().getBrokerClusterName(), + master1With3Replicas.getBrokerIdentity().getBrokerName(), + master1With3Replicas.getBrokerIdentity().getBrokerId())); + System.out.printf("=========" + master1With3Replicas.getBrokerIdentity().getBrokerName() + "-" + + master1With3Replicas.getBrokerIdentity().getBrokerId() + " removed%n"); + + createTopicTo(master2With3Replicas, topic, 1, 1); + createTopicTo(master3With3Replicas, topic, 1, 1); + //isolateBroker(master2With3Replicas); + brokerContainer2.removeBroker(new BrokerIdentity(master2With3Replicas.getBrokerIdentity().getBrokerClusterName(), + master2With3Replicas.getBrokerIdentity().getBrokerName(), + master2With3Replicas.getBrokerIdentity().getBrokerId())); + System.out.printf("=========" + master2With3Replicas.getBrokerIdentity().getBrokerClusterName() + "-" + + master2With3Replicas.getBrokerIdentity().getBrokerName() + + "-" + master2With3Replicas.getBrokerIdentity().getBrokerId() + " removed%n"); + + pushConsumer.getDefaultMQPushConsumerImpl().getRebalanceImpl().doRebalance(false); + transactionCheckListener.setShouldReturnUnknownState(false); + producer.getDefaultMQProducerImpl().getmQClientFactory().updateTopicRouteInfoFromNameServer(topic); + + System.out.printf("Wait for consuming%n"); + + await().atMost(Duration.ofSeconds(180)).until(() -> receivedMsgCount.get() >= MESSAGE_COUNT); + + System.out.printf("consumer received %d msg%n", receivedMsgCount.get()); + + pushConsumer.shutdown(); + producer.shutdown(); + + master1With3Replicas = brokerContainer1.addBroker(master1With3Replicas.getBrokerConfig(), master1With3Replicas.getMessageStoreConfig()); + master1With3Replicas.start(); + cancelIsolatedBroker(master1With3Replicas); + + master2With3Replicas = brokerContainer2.addBroker(master2With3Replicas.getBrokerConfig(), + master2With3Replicas.getMessageStoreConfig()); + master2With3Replicas.start(); + cancelIsolatedBroker(master2With3Replicas); + + awaitUntilSlaveOK(); + + receivedMsgCount.set(0); + DefaultMQPushConsumer pushConsumer2 = createPushConsumer(group); + pushConsumer2.subscribe(topic, "*"); + pushConsumer2.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + for (MessageExt msg : msgs) { + if (msgSentMap.containsKey(msg.getMsgId())) { + receivedMsgCount.incrementAndGet(); + } + } + + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + pushConsumer2.start(); + System.out.printf("Wait for checking...%n"); + Thread.sleep(10000L); + assertThat(receivedMsgCount.get()).isEqualTo(0); + pushConsumer2.shutdown(); + + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/delay/DelayConf.java b/test/src/test/java/org/apache/rocketmq/test/delay/DelayConf.java new file mode 100644 index 0000000..6c23473 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/delay/DelayConf.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.test.delay; + +import org.apache.rocketmq.test.base.BaseConf; + +public class DelayConf extends BaseConf { + protected static final int[] DELAY_LEVEL = { + 1, 5, 10, 30, 1 * 60, 5 * 60, 10 * 60, + 30 * 60, 1 * 3600, 2 * 3600, 6 * 3600, 12 * 3600, 1 * 24 * 3600}; + +} diff --git a/test/src/test/java/org/apache/rocketmq/test/delay/NormalMsgDelayIT.java b/test/src/test/java/org/apache/rocketmq/test/delay/NormalMsgDelayIT.java new file mode 100644 index 0000000..06330a1 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/delay/NormalMsgDelayIT.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.test.delay; + +import java.util.List; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.factory.MQMessageFactory; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQDelayListener; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class NormalMsgDelayIT extends DelayConf { + private static Logger logger = LoggerFactory.getLogger(NormalMsgDelayIT.class); + protected int msgSize = 100; + private RMQNormalProducer producer = null; + private RMQNormalConsumer consumer = null; + private String topic = null; + + @Before + public void setUp() { + topic = initTopic(); + logger.info(String.format("use topic: %s;", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + consumer = getConsumer(NAMESRV_ADDR, topic, "*", new RMQDelayListener()); + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testDelayLevel1() throws Exception { + Thread.sleep(3000); + int delayLevel = 1; + List delayMsgs = MQMessageFactory.getDelayMsg(topic, delayLevel, msgSize); + producer.send(delayMsgs); + Assert.assertEquals("Not all sent succeeded", msgSize, producer.getAllUndupMsgBody().size()); + + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + Assert.assertEquals("Not all are consumed", 0, VerifyUtils.verify(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())); + Assert.assertEquals("Timer is not correct", true, + VerifyUtils.verifyDelay(DELAY_LEVEL[delayLevel - 1] * 1000, DELAY_LEVEL[delayLevel] * 1000, + ((RMQDelayListener) consumer.getListener()).getMsgDelayTimes())); + } + + @Test + public void testDelayLevel2() { + int delayLevel = 2; + List delayMsgs = MQMessageFactory.getDelayMsg(topic, delayLevel, msgSize); + producer.send(delayMsgs); + Assert.assertEquals("Not all sent succeeded", msgSize, producer.getAllUndupMsgBody().size()); + + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), + DELAY_LEVEL[delayLevel - 1] * 1000 * 2); + Assert.assertEquals("Not all are consumed", 0, VerifyUtils.verify(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())); + Assert.assertEquals("Timer is not correct", true, + VerifyUtils.verifyDelay(DELAY_LEVEL[delayLevel - 1] * 1000, DELAY_LEVEL[delayLevel] * 1000, + ((RMQDelayListener) consumer.getListener()).getMsgDelayTimes())); + } + + @Test + public void testDelayLevel3() { + int delayLevel = 3; + List delayMsgs = MQMessageFactory.getDelayMsg(topic, delayLevel, msgSize); + producer.send(delayMsgs); + Assert.assertEquals("Not all are sent", msgSize, producer.getAllUndupMsgBody().size()); + + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), + DELAY_LEVEL[delayLevel - 1] * 1000 * 2); + Assert.assertEquals("Not all are consumed", 0, VerifyUtils.verify(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())); + Assert.assertEquals("Timer is not correct", true, + VerifyUtils.verifyDelay(DELAY_LEVEL[delayLevel - 1] * 1000, DELAY_LEVEL[delayLevel] * 1000, + ((RMQDelayListener) consumer.getListener()).getMsgDelayTimes())); + } + + @Test + public void testDelayLevel4() { + int delayLevel = 4; + List delayMsgs = MQMessageFactory.getDelayMsg(topic, delayLevel, msgSize); + producer.send(delayMsgs); + Assert.assertEquals("Not all are sent", msgSize, producer.getAllUndupMsgBody().size()); + + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), + DELAY_LEVEL[delayLevel - 1] * 1000 * 2); + Assert.assertEquals("Not all are consumed", 0, VerifyUtils.verify(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())); + Assert.assertEquals("Timer is not correct", true, + VerifyUtils.verifyDelay(DELAY_LEVEL[delayLevel - 1] * 1000, DELAY_LEVEL[delayLevel] * 1000, + ((RMQDelayListener) consumer.getListener()).getMsgDelayTimes())); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/dledger/DLedgerProduceAndConsumeIT.java b/test/src/test/java/org/apache/rocketmq/test/dledger/DLedgerProduceAndConsumeIT.java new file mode 100644 index 0000000..43fefd6 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/dledger/DLedgerProduceAndConsumeIT.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.test.dledger; + +import java.util.UUID; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +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.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.BrokerConfig; +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.store.config.BrokerRole; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.common.attribute.CQType; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.base.IntegrationTestBase; +import org.apache.rocketmq.test.factory.ConsumerFactory; +import org.apache.rocketmq.test.factory.ProducerFactory; +import org.junit.Assert; +import org.junit.Test; + +import static sun.util.locale.BaseLocale.SEP; + +public class DLedgerProduceAndConsumeIT { + + public BrokerConfig buildBrokerConfig(String cluster, String brokerName) { + BrokerConfig brokerConfig = new BrokerConfig(); + brokerConfig.setBrokerClusterName(cluster); + brokerConfig.setBrokerName(brokerName); + brokerConfig.setBrokerIP1("127.0.0.1"); + brokerConfig.setNamesrvAddr(BaseConf.NAMESRV_ADDR); + return brokerConfig; + } + + public MessageStoreConfig buildStoreConfig(String brokerName, String peers, String selfId) { + MessageStoreConfig storeConfig = new MessageStoreConfig(); + String baseDir = IntegrationTestBase.createBaseDir(); + storeConfig.setStorePathRootDir(baseDir); + storeConfig.setStorePathCommitLog(baseDir + SEP + "commitlog"); + storeConfig.setHaListenPort(0); + storeConfig.setMappedFileSizeCommitLog(10 * 1024 * 1024); + storeConfig.setEnableDLegerCommitLog(true); + storeConfig.setdLegerGroup(brokerName); + storeConfig.setdLegerSelfId(selfId); + storeConfig.setdLegerPeers(peers); + return storeConfig; + } + + @Test + public void testProduceAndConsume() throws Exception { + String cluster = UUID.randomUUID().toString(); + String brokerName = UUID.randomUUID().toString(); + String selfId = "n0"; + // TODO: We need to acquire the actual listening port after the peer has started. + String peers = String.format("n0-localhost:%d", 0); + BrokerConfig brokerConfig = buildBrokerConfig(cluster, brokerName); + MessageStoreConfig storeConfig = buildStoreConfig(brokerName, peers, selfId); + BrokerController brokerController = IntegrationTestBase.createAndStartBroker(storeConfig, brokerConfig); + BaseConf.waitBrokerRegistered(BaseConf.NAMESRV_ADDR, brokerConfig.getBrokerName(), 1); + + Assert.assertEquals(BrokerRole.SYNC_MASTER, storeConfig.getBrokerRole()); + + + String topic = UUID.randomUUID().toString(); + String consumerGroup = UUID.randomUUID().toString(); + IntegrationTestBase.initTopic(topic, BaseConf.NAMESRV_ADDR, cluster, 1, CQType.SimpleCQ); + DefaultMQProducer producer = ProducerFactory.getRMQProducer(BaseConf.NAMESRV_ADDR); + DefaultMQPullConsumer consumer = ConsumerFactory.getRMQPullConsumer(BaseConf.NAMESRV_ADDR, consumerGroup); + + for (int i = 0; i < 10; i++) { + Message message = new Message(); + message.setTopic(topic); + message.setBody(("Hello" + i).getBytes()); + SendResult sendResult = producer.send(message); + Assert.assertEquals(SendStatus.SEND_OK, sendResult.getSendStatus()); + Assert.assertEquals(0, sendResult.getMessageQueue().getQueueId()); + Assert.assertEquals(brokerName, sendResult.getMessageQueue().getBrokerName()); + Assert.assertEquals(i, sendResult.getQueueOffset()); + Assert.assertNotNull(sendResult.getMsgId()); + Assert.assertNotNull(sendResult.getOffsetMsgId()); + } + + Thread.sleep(500); + Assert.assertEquals(0, brokerController.getMessageStore().getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(10, brokerController.getMessageStore().getMaxOffsetInQueue(topic, 0)); + + MessageQueue messageQueue = new MessageQueue(topic, brokerName, 0); + PullResult pullResult = consumer.pull(messageQueue, "*", 0, 32); + Assert.assertEquals(PullStatus.FOUND, pullResult.getPullStatus()); + Assert.assertEquals(10, pullResult.getMsgFoundList().size()); + + for (int i = 0; i < 10; i++) { + MessageExt messageExt = pullResult.getMsgFoundList().get(i); + Assert.assertEquals(i, messageExt.getQueueOffset()); + Assert.assertArrayEquals(("Hello" + i).getBytes(), messageExt.getBody()); + } + + producer.shutdown(); + consumer.shutdown(); + brokerController.shutdown(); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/grpc/v2/ClusterGrpcIT.java b/test/src/test/java/org/apache/rocketmq/test/grpc/v2/ClusterGrpcIT.java new file mode 100644 index 0000000..b754466 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/grpc/v2/ClusterGrpcIT.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.test.grpc.v2; + +import apache.rocketmq.v2.QueryRouteResponse; +import java.time.Duration; +import java.util.Map; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.grpc.v2.GrpcMessagingApplication; +import org.apache.rocketmq.proxy.processor.DefaultMessagingProcessor; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.test.util.MQAdminTestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.Ignore; +import org.junit.runners.MethodSorters; + +import static org.awaitility.Awaitility.await; + +@FixMethodOrder(value = MethodSorters.NAME_ASCENDING) +public class ClusterGrpcIT extends GrpcBaseIT { + + private MessagingProcessor messagingProcessor; + private GrpcMessagingApplication grpcMessagingApplication; + + @Before + public void setUp() throws Exception { + super.setUp(); + ConfigurationManager.getProxyConfig().setTransactionHeartbeatPeriodSecond(3); + messagingProcessor = DefaultMessagingProcessor.createForClusterMode(); + messagingProcessor.start(); + grpcMessagingApplication = GrpcMessagingApplication.create(messagingProcessor); + grpcMessagingApplication.start(); + setUpServer(grpcMessagingApplication, 0, true); + + await().atMost(Duration.ofSeconds(40)).until(() -> { + Map brokerDataMap = MQAdminTestUtils.getCluster(NAMESRV_ADDR).getBrokerAddrTable(); + return brokerDataMap.size() == BROKER_NUM; + }); + } + + @After + public void tearDown() throws Exception { + messagingProcessor.shutdown(); + grpcMessagingApplication.shutdown(); + shutdown(); + } + + @Test + public void testQueryRoute() throws Exception { + String topic = initTopic(); + + QueryRouteResponse response = blockingStub.queryRoute(buildQueryRouteRequest(topic)); + assertQueryRoute(response, BROKER_NUM * DEFAULT_QUEUE_NUMS); + } + + @Test + public void testQueryAssignment() throws Exception { + super.testQueryAssignment(); + } + + @Test + public void testQueryFifoAssignment() throws Exception { + super.testQueryFifoAssignment(); + } + + @Test + public void testTransactionCheckThenCommit() { + super.testTransactionCheckThenCommit(); + } + + @Test + @Ignore + public void testSimpleConsumerSendAndRecvDelayMessage() throws Exception { + super.testSimpleConsumerSendAndRecvDelayMessage(); + } + + @Test + public void testSimpleConsumerSendAndRecallDelayMessage() throws Exception { + super.testSimpleConsumerSendAndRecallDelayMessage(); + } + + @Test + public void testSimpleConsumerSendAndRecvBigMessage() throws Exception { + super.testSimpleConsumerSendAndRecvBigMessage(); + } + + @Test + public void testSimpleConsumerSendAndRecv() throws Exception { + super.testSimpleConsumerSendAndRecv(); + } + + @Test + public void testSimpleConsumerToDLQ() throws Exception { + super.testSimpleConsumerToDLQ(); + } + + @Test + public void testConsumeOrderly() throws Exception { + super.testConsumeOrderly(); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/grpc/v2/GrpcBaseIT.java b/test/src/test/java/org/apache/rocketmq/test/grpc/v2/GrpcBaseIT.java new file mode 100644 index 0000000..534108c --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/grpc/v2/GrpcBaseIT.java @@ -0,0 +1,909 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.test.grpc.v2; + +import apache.rocketmq.v2.AckMessageEntry; +import apache.rocketmq.v2.AckMessageRequest; +import apache.rocketmq.v2.AckMessageResponse; +import apache.rocketmq.v2.AckMessageResultEntry; +import apache.rocketmq.v2.Address; +import apache.rocketmq.v2.AddressScheme; +import apache.rocketmq.v2.ChangeInvisibleDurationRequest; +import apache.rocketmq.v2.ChangeInvisibleDurationResponse; +import apache.rocketmq.v2.ClientType; +import apache.rocketmq.v2.Code; +import apache.rocketmq.v2.Encoding; +import apache.rocketmq.v2.EndTransactionRequest; +import apache.rocketmq.v2.EndTransactionResponse; +import apache.rocketmq.v2.Endpoints; +import apache.rocketmq.v2.HeartbeatRequest; +import apache.rocketmq.v2.Message; +import apache.rocketmq.v2.MessageQueue; +import apache.rocketmq.v2.MessageType; +import apache.rocketmq.v2.MessagingServiceGrpc; +import apache.rocketmq.v2.Publishing; +import apache.rocketmq.v2.QueryAssignmentRequest; +import apache.rocketmq.v2.QueryAssignmentResponse; +import apache.rocketmq.v2.QueryRouteRequest; +import apache.rocketmq.v2.QueryRouteResponse; +import apache.rocketmq.v2.RecallMessageRequest; +import apache.rocketmq.v2.RecallMessageResponse; +import apache.rocketmq.v2.ReceiveMessageRequest; +import apache.rocketmq.v2.ReceiveMessageResponse; +import apache.rocketmq.v2.RecoverOrphanedTransactionCommand; +import apache.rocketmq.v2.Resource; +import apache.rocketmq.v2.RetryPolicy; +import apache.rocketmq.v2.SendMessageRequest; +import apache.rocketmq.v2.SendMessageResponse; +import apache.rocketmq.v2.Settings; +import apache.rocketmq.v2.Subscription; +import apache.rocketmq.v2.SystemProperties; +import apache.rocketmq.v2.TelemetryCommand; +import apache.rocketmq.v2.TransactionResolution; +import apache.rocketmq.v2.TransactionSource; +import com.google.protobuf.ByteString; +import com.google.protobuf.Duration; +import com.google.protobuf.util.Durations; +import com.google.protobuf.util.Timestamps; +import io.grpc.Channel; +import io.grpc.Metadata; +import io.grpc.Server; +import io.grpc.ServerInterceptors; +import io.grpc.ServerServiceDefinition; +import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; +import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; +import io.grpc.netty.shaded.io.netty.handler.ssl.ApplicationProtocolConfig; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslProvider; +import io.grpc.stub.MetadataUtils; +import io.grpc.stub.StreamObserver; +import io.grpc.testing.GrpcCleanupRule; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import java.io.IOException; +import java.net.URL; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import javax.net.ssl.SSLException; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +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.attribute.TopicMessageType; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.grpc.interceptor.ContextInterceptor; +import org.apache.rocketmq.proxy.grpc.interceptor.HeaderInterceptor; +import org.apache.rocketmq.common.constant.GrpcConstants; +import org.apache.rocketmq.proxy.grpc.v2.common.ResponseBuilder; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.util.MQRandomUtils; +import org.apache.rocketmq.test.util.RandomUtils; +import org.junit.Rule; + +import static org.apache.rocketmq.common.message.MessageClientIDSetter.createUniqID; +import static org.apache.rocketmq.proxy.config.ConfigurationManager.RMQ_PROXY_HOME; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public class GrpcBaseIT extends BaseConf { + + /** + * Let OS pick up an available port. + */ + private int port = 0; + + /** + * This rule manages automatic graceful shutdown for the registered servers and channels at the end of test. + */ + @Rule + public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + + protected MessagingServiceGrpc.MessagingServiceBlockingStub blockingStub; + protected MessagingServiceGrpc.MessagingServiceStub stub; + protected final Metadata header = new Metadata(); + + protected static final int DEFAULT_QUEUE_NUMS = 8; + + public void setUp() throws Exception { + brokerController1.getBrokerConfig().setTransactionCheckInterval(3 * 1000); + brokerController2.getBrokerConfig().setTransactionCheckInterval(3 * 1000); + brokerController3.getBrokerConfig().setTransactionCheckInterval(3 * 1000); + + header.put(GrpcConstants.CLIENT_ID, "client-id" + UUID.randomUUID()); + header.put(GrpcConstants.LANGUAGE, "JAVA"); + + String mockProxyHome = "/mock/rmq/proxy/home"; + URL mockProxyHomeURL = getClass().getClassLoader().getResource("rmq-proxy-home"); + if (mockProxyHomeURL != null) { + mockProxyHome = mockProxyHomeURL.toURI().getPath(); + } + + if (null != mockProxyHome) { + System.setProperty(RMQ_PROXY_HOME, mockProxyHome); + } + + ConfigurationManager.initEnv(); + ConfigurationManager.intConfig(); + ConfigurationManager.getProxyConfig().setNamesrvAddr(NAMESRV_ADDR); + // Set LongPollingReserveTimeInMillis to 500ms to reserve more time for IT + ConfigurationManager.getProxyConfig().setLongPollingReserveTimeInMillis(500); + ConfigurationManager.getProxyConfig().setRocketMQClusterName(brokerController1.getBrokerConfig().getBrokerClusterName()); + ConfigurationManager.getProxyConfig().setHeartbeatSyncerTopicClusterName(brokerController1.getBrokerConfig().getBrokerClusterName()); + ConfigurationManager.getProxyConfig().setMinInvisibleTimeMillsForRecv(3); + ConfigurationManager.getProxyConfig().setGrpcClientConsumerMinLongPollingTimeoutMillis(0); + } + + protected MessagingServiceGrpc.MessagingServiceStub createStub(Channel channel) { + MessagingServiceGrpc.MessagingServiceStub stub = MessagingServiceGrpc.newStub(channel); + return stub.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(header)); + } + + protected MessagingServiceGrpc.MessagingServiceBlockingStub createBlockingStub(Channel channel) { + MessagingServiceGrpc.MessagingServiceBlockingStub stub = MessagingServiceGrpc.newBlockingStub(channel); + return stub.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(header)); + } + + protected CompletableFuture sendClientSettings(MessagingServiceGrpc.MessagingServiceStub stub, + Settings clientSettings) { + CompletableFuture future = new CompletableFuture<>(); + StreamObserver requestStreamObserver = stub.telemetry(new DefaultTelemetryCommandStreamObserver() { + @Override + public void onNext(TelemetryCommand value) { + TelemetryCommand.CommandCase commandCase = value.getCommandCase(); + if (TelemetryCommand.CommandCase.SETTINGS.equals(commandCase)) { + future.complete(value.getSettings()); + } + } + }); + requestStreamObserver.onNext(TelemetryCommand.newBuilder() + .setSettings(clientSettings) + .build()); + future.whenComplete((settings, throwable) -> requestStreamObserver.onCompleted()); + return future; + } + + protected void setUpServer(MessagingServiceGrpc.MessagingServiceImplBase serverImpl, + int port, boolean enableInterceptor) throws IOException, CertificateException { + SelfSignedCertificate selfSignedCertificate = new SelfSignedCertificate(); + ServerServiceDefinition serviceDefinition = ServerInterceptors.intercept(serverImpl); + if (enableInterceptor) { + serviceDefinition = ServerInterceptors.intercept(serverImpl, new ContextInterceptor(), new HeaderInterceptor()); + } + Server server = NettyServerBuilder.forPort(port) + .directExecutor() + .addService(serviceDefinition) + .useTransportSecurity(selfSignedCertificate.certificate(), selfSignedCertificate.privateKey()) + .build() + .start(); + this.port = server.getPort(); + // Create a server, add service, start, and register for automatic graceful shutdown. + grpcCleanup.register(server); + + ConfigurationManager.getProxyConfig().setGrpcServerPort(this.port); + blockingStub = createBlockingStub(createChannel(ConfigurationManager.getProxyConfig().getGrpcServerPort())); + stub = createStub(createChannel(ConfigurationManager.getProxyConfig().getGrpcServerPort())); + } + + protected Channel createChannel(int port) throws SSLException { + return grpcCleanup.register(NettyChannelBuilder.forAddress("127.0.0.1", port) + .directExecutor() + .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() + ) + .build()); + } + + public void testQueryAssignment() throws Exception { + String topic = initTopic(); + String group = "group"; + + QueryAssignmentResponse response = blockingStub.queryAssignment(buildQueryAssignmentRequest(topic, group)); + + assertQueryAssignment(response, BROKER_NUM); + } + + public void testQueryFifoAssignment() throws Exception { + String topic = initTopic(TopicMessageType.FIFO); + String group = MQRandomUtils.getRandomConsumerGroup(); + SubscriptionGroupConfig groupConfig = brokerController1.getSubscriptionGroupManager().findSubscriptionGroupConfig(group); + groupConfig.setConsumeMessageOrderly(true); + brokerController1.getSubscriptionGroupManager().updateSubscriptionGroupConfig(groupConfig); + brokerController2.getSubscriptionGroupManager().updateSubscriptionGroupConfig(groupConfig); + brokerController3.getSubscriptionGroupManager().updateSubscriptionGroupConfig(groupConfig); + + QueryAssignmentResponse response = blockingStub.queryAssignment(buildQueryAssignmentRequest(topic, group)); + + assertQueryAssignment(response, BROKER_NUM * QUEUE_NUMBERS); + } + + public void testTransactionCheckThenCommit() { + String topic = initTopicOnSampleTopicBroker(BROKER1_NAME, TopicMessageType.TRANSACTION); + String group = MQRandomUtils.getRandomConsumerGroup(); + + AtomicReference telemetryCommandRef = new AtomicReference<>(null); + StreamObserver requestStreamObserver = stub.telemetry(new DefaultTelemetryCommandStreamObserver() { + @Override + public void onNext(TelemetryCommand value) { + telemetryCommandRef.set(value); + } + }); + + try { + requestStreamObserver.onNext(TelemetryCommand.newBuilder() + .setSettings(buildPushConsumerClientSettings(group)) + .build()); + await().atMost(java.time.Duration.ofSeconds(3)).until(() -> { + if (telemetryCommandRef.get() == null) { + return false; + } + if (telemetryCommandRef.get().getCommandCase() != TelemetryCommand.CommandCase.SETTINGS) { + return false; + } + return telemetryCommandRef.get() != null; + }); + telemetryCommandRef.set(null); + // init consumer offset + receiveMessage(blockingStub, topic, group, 1); + + requestStreamObserver.onNext(TelemetryCommand.newBuilder() + .setSettings(buildProducerClientSettings(topic)) + .build()); + blockingStub.heartbeat(buildHeartbeatRequest(group)); + await().atMost(java.time.Duration.ofSeconds(3)).until(() -> { + if (telemetryCommandRef.get() == null) { + blockingStub.heartbeat(buildHeartbeatRequest(group)); + return false; + } + if (telemetryCommandRef.get().getCommandCase() != TelemetryCommand.CommandCase.SETTINGS) { + blockingStub.heartbeat(buildHeartbeatRequest(group)); + return false; + } + return telemetryCommandRef.get() != null; + }); + telemetryCommandRef.set(null); + + String messageId = createUniqID(); + SendMessageResponse sendResponse = blockingStub.sendMessage(buildTransactionSendMessageRequest(topic, messageId)); + assertSendMessage(sendResponse, messageId); + + await().atMost(java.time.Duration.ofMinutes(2)).until(() -> { + if (telemetryCommandRef.get() == null) { + blockingStub.heartbeat(buildHeartbeatRequest(group)); + return false; + } + if (telemetryCommandRef.get().getCommandCase() != TelemetryCommand.CommandCase.RECOVER_ORPHANED_TRANSACTION_COMMAND) { + blockingStub.heartbeat(buildHeartbeatRequest(group)); + return false; + } + return telemetryCommandRef.get() != null; + }); + RecoverOrphanedTransactionCommand recoverOrphanedTransactionCommand = telemetryCommandRef.get().getRecoverOrphanedTransactionCommand(); + assertRecoverOrphanedTransactionCommand(recoverOrphanedTransactionCommand, messageId); + + EndTransactionResponse endTransactionResponse = blockingStub.endTransaction( + buildEndTransactionRequest(topic, messageId, recoverOrphanedTransactionCommand.getTransactionId(), TransactionResolution.COMMIT)); + assertEndTransactionResponse(endTransactionResponse); + + requestStreamObserver.onNext(TelemetryCommand.newBuilder() + .setSettings(buildPushConsumerClientSettings(group)) + .build()); + + await().atMost(java.time.Duration.ofSeconds(30)).until(() -> { + List retryMessageList = getMessageFromReceiveMessageResponse(receiveMessage(blockingStub, topic, group)); + if (retryMessageList.isEmpty()) { + return false; + } + return retryMessageList.get(0).getSystemProperties() + .getMessageId().equals(messageId); + }); + } finally { + requestStreamObserver.onCompleted(); + } + } + + public HeartbeatRequest buildHeartbeatRequest(String group) { + return HeartbeatRequest.newBuilder() + .setGroup(Resource.newBuilder() + .setName(group) + .build()) + .build(); + } + + public void testSimpleConsumerSendAndRecvDelayMessage() throws Exception { + String topic = initTopicOnSampleTopicBroker(BROKER1_NAME, TopicMessageType.DELAY); + String group = MQRandomUtils.getRandomConsumerGroup(); + long delayTime = TimeUnit.SECONDS.toMillis(5); + + // init consumer offset + this.sendClientSettings(stub, buildSimpleConsumerClientSettings(group)).get(); + receiveMessage(blockingStub, topic, group, 1); + + this.sendClientSettings(stub, buildProducerClientSettings(topic)).get(); + String messageId = createUniqID(); + SendMessageResponse sendResponse = blockingStub.sendMessage(SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(topic) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId(messageId) + .setQueueId(0) + .setMessageType(MessageType.NORMAL) + .setBodyEncoding(Encoding.GZIP) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .setDeliveryTimestamp(Timestamps.fromMillis(System.currentTimeMillis() + delayTime)) + .build()) + .setBody(ByteString.copyFromUtf8("hello")) + .build()) + .build()); + long sendTime = System.currentTimeMillis(); + assertSendMessage(sendResponse, messageId); + + this.sendClientSettings(stub, buildSimpleConsumerClientSettings(group)).get(); + + AtomicLong recvTime = new AtomicLong(); + AtomicReference recvMessage = new AtomicReference<>(); + await().atMost(java.time.Duration.ofSeconds(10)).until(() -> { + List messageList = getMessageFromReceiveMessageResponse(receiveMessage(blockingStub, topic, group)); + if (messageList.isEmpty()) { + return false; + } + recvTime.set(System.currentTimeMillis()); + recvMessage.set(messageList.get(0)); + return messageList.get(0).getSystemProperties().getMessageId().equals(messageId); + }); + + assertThat(Math.abs(recvTime.get() - sendTime - delayTime) < 2 * 1000).isTrue(); + } + + public void testSimpleConsumerSendAndRecallDelayMessage() throws Exception { + String topic = initTopicOnSampleTopicBroker(BROKER1_NAME, TopicMessageType.DELAY); + String group = MQRandomUtils.getRandomConsumerGroup(); + long delayTime = TimeUnit.SECONDS.toMillis(5); + + // init consumer offset + this.sendClientSettings(stub, buildSimpleConsumerClientSettings(group)).get(); + receiveMessage(blockingStub, topic, group, 1); + + this.sendClientSettings(stub, buildProducerClientSettings(topic)).get(); + String messageId = createUniqID(); + SendMessageResponse sendResponse = blockingStub.sendMessage(SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(topic) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId(messageId) + .setQueueId(0) + .setMessageType(MessageType.DELAY) + .setBodyEncoding(Encoding.GZIP) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .setDeliveryTimestamp(Timestamps.fromMillis(System.currentTimeMillis() + delayTime)) + .build()) + .setBody(ByteString.copyFromUtf8("hello")) + .build()) + .build()); + long sendTime = System.currentTimeMillis(); + assertSendMessage(sendResponse, messageId); + String recallHandle = sendResponse.getEntries(0).getRecallHandle(); + assertThat(recallHandle).isNotEmpty(); + + RecallMessageRequest recallRequest = RecallMessageRequest.newBuilder() + .setRecallHandle(recallHandle) + .setTopic(Resource.newBuilder().setResourceNamespace("").setName(topic).build()) + .build(); + RecallMessageResponse recallResponse = + blockingStub.withDeadlineAfter(2, TimeUnit.SECONDS).recallMessage(recallRequest); + assertThat(recallResponse.getStatus()).isEqualTo( + ResponseBuilder.getInstance().buildStatus(Code.OK, Code.OK.name())); + assertThat(recallResponse.getMessageId()).isEqualTo(messageId); + + this.sendClientSettings(stub, buildSimpleConsumerClientSettings(group)).get(); + + AtomicLong recvTime = new AtomicLong(); + AtomicReference recvMessage = new AtomicReference<>(); + try { + await().atMost(java.time.Duration.ofSeconds(10)).until(() -> { + List messageList = getMessageFromReceiveMessageResponse(receiveMessage(blockingStub, topic, group)); + if (messageList.isEmpty()) { + return false; + } + recvTime.set(System.currentTimeMillis()); + recvMessage.set(messageList.get(0)); + return messageList.get(0).getSystemProperties().getMessageId().equals(messageId); + }); + } catch (Exception e) { + } + assertThat(recvTime.get()).isEqualTo(0L); + assertThat(recvMessage.get()).isNull(); + } + + public void testSimpleConsumerSendAndRecvBigMessage() throws Exception { + String topic = initTopicOnSampleTopicBroker(BROKER1_NAME); + String group = MQRandomUtils.getRandomConsumerGroup(); + + int bodySize = 4 * 1024; + + // init consumer offset + this.sendClientSettings(stub, buildSimpleConsumerClientSettings(group)).get(); + receiveMessage(blockingStub, topic, group, 1); + + this.sendClientSettings(stub, buildProducerClientSettings(topic)).get(); + String messageId = createUniqID(); + SendMessageResponse sendResponse = blockingStub.sendMessage(buildSendBigMessageRequest(topic, messageId, bodySize)); + assertSendMessage(sendResponse, messageId); + + this.sendClientSettings(stub, buildSimpleConsumerClientSettings(group)).get(); + + Message message = assertAndGetReceiveMessage(receiveMessage(blockingStub, topic, group), messageId); + assertThat(message.getSystemProperties().getBodyEncoding()).isEqualTo(Encoding.GZIP); + assertThat(message.getBody().size()).isEqualTo(bodySize); + } + + public void testSimpleConsumerSendAndRecv() throws Exception { + String topic = initTopicOnSampleTopicBroker(BROKER1_NAME); + String group = MQRandomUtils.getRandomConsumerGroup(); + + // init consumer offset + this.sendClientSettings(stub, buildSimpleConsumerClientSettings(group)).get(); + receiveMessage(blockingStub, topic, group, 1); + + this.sendClientSettings(stub, buildProducerClientSettings(topic)).get(); + String messageId = createUniqID(); + SendMessageResponse sendResponse = blockingStub.sendMessage(buildSendMessageRequest(topic, messageId)); + assertSendMessage(sendResponse, messageId); + assertThat(sendResponse.getEntries(0).getRecallHandle()).isNullOrEmpty(); + + this.sendClientSettings(stub, buildSimpleConsumerClientSettings(group)).get(); + + Message message = assertAndGetReceiveMessage(receiveMessage(blockingStub, topic, group), messageId); + + String receiptHandle = message.getSystemProperties().getReceiptHandle(); + ChangeInvisibleDurationResponse changeResponse = blockingStub.changeInvisibleDuration(buildChangeInvisibleDurationRequest(topic, group, receiptHandle, 5)); + assertChangeInvisibleDurationResponse(changeResponse, receiptHandle); + + List ackHandles = new ArrayList<>(); + ackHandles.add(changeResponse.getReceiptHandle()); + + await().atMost(java.time.Duration.ofSeconds(20)).until(() -> { + List retryMessageList = getMessageFromReceiveMessageResponse(receiveMessage(blockingStub, topic, group)); + if (retryMessageList.isEmpty()) { + return false; + } + if (retryMessageList.get(0).getSystemProperties() + .getMessageId().equals(messageId)) { + ackHandles.add(retryMessageList.get(0).getSystemProperties().getReceiptHandle()); + return true; + } + return false; + }); + + assertThat(ackHandles.size()).isEqualTo(2); + AckMessageResponse ackMessageResponse = blockingStub.ackMessage(buildAckMessageRequest(topic, group, + AckMessageEntry.newBuilder().setMessageId(messageId).setReceiptHandle(ackHandles.get(0)).build(), + AckMessageEntry.newBuilder().setMessageId(messageId).setReceiptHandle(ackHandles.get(1)).build())); + assertThat(ackMessageResponse.getStatus().getCode()).isEqualTo(Code.MULTIPLE_RESULTS); + int okNum = 0; + int expireNum = 0; + for (AckMessageResultEntry entry : ackMessageResponse.getEntriesList()) { + if (entry.getStatus().getCode().equals(Code.OK)) { + okNum++; + } else if (entry.getStatus().getCode().equals(Code.INVALID_RECEIPT_HANDLE)) { + expireNum++; + } + } + assertThat(okNum).isEqualTo(1); + assertThat(expireNum).isEqualTo(1); + } + + public void testSimpleConsumerToDLQ() throws Exception { + String topic = initTopicOnSampleTopicBroker(BROKER1_NAME); + String group = MQRandomUtils.getRandomConsumerGroup(); + int maxDeliveryAttempts = 2; + + SubscriptionGroupConfig groupConfig = brokerController1.getSubscriptionGroupManager().findSubscriptionGroupConfig(group); + groupConfig.setRetryMaxTimes(maxDeliveryAttempts - 1); + brokerController1.getSubscriptionGroupManager().updateSubscriptionGroupConfig(groupConfig); + brokerController2.getSubscriptionGroupManager().updateSubscriptionGroupConfig(groupConfig); + brokerController3.getSubscriptionGroupManager().updateSubscriptionGroupConfig(groupConfig); + + // init consumer offset + this.sendClientSettings(stub, buildSimpleConsumerClientSettings(group)).get(); + receiveMessage(blockingStub, topic, group, 1); + + this.sendClientSettings(stub, buildProducerClientSettings(topic)).get(); + String messageId = createUniqID(); + SendMessageResponse sendResponse = blockingStub.sendMessage(buildSendMessageRequest(topic, messageId)); + assertSendMessage(sendResponse, messageId); + + this.sendClientSettings(stub, buildSimpleConsumerClientSettings(group)).get(); + + AtomicInteger receiveMessageCount = new AtomicInteger(0); + + assertAndGetReceiveMessage(receiveMessage(blockingStub, topic, group), messageId); + receiveMessageCount.incrementAndGet(); + + DefaultMQPullConsumer defaultMQPullConsumer = new DefaultMQPullConsumer(group); + defaultMQPullConsumer.start(); + org.apache.rocketmq.common.message.MessageQueue dlqMQ = new org.apache.rocketmq.common.message.MessageQueue(MixAll.getDLQTopic(group), BROKER1_NAME, 0); + await().atMost(java.time.Duration.ofSeconds(30)).until(() -> { + try { + List messageList = getMessageFromReceiveMessageResponse(receiveMessage(blockingStub, topic, group, 1)); + receiveMessageCount.addAndGet(messageList.size()); + + PullResult pullResult = defaultMQPullConsumer.pull(dlqMQ, "*", 0L, 1); + if (!PullStatus.FOUND.equals(pullResult.getPullStatus())) { + return false; + } + MessageExt messageExt = pullResult.getMsgFoundList().get(0); + return messageId.equals(messageExt.getMsgId()); + } catch (Throwable ignore) { + return false; + } + }); + + assertThat(receiveMessageCount.get()).isEqualTo(maxDeliveryAttempts); + } + + public void testConsumeOrderly() throws Exception { + String topic = initTopicOnSampleTopicBroker(BROKER1_NAME, TopicMessageType.FIFO); + String group = MQRandomUtils.getRandomConsumerGroup(); + + SubscriptionGroupConfig groupConfig = brokerController1.getSubscriptionGroupManager().findSubscriptionGroupConfig(group); + groupConfig.setConsumeMessageOrderly(true); + brokerController1.getSubscriptionGroupManager().updateSubscriptionGroupConfig(groupConfig); + brokerController2.getSubscriptionGroupManager().updateSubscriptionGroupConfig(groupConfig); + brokerController3.getSubscriptionGroupManager().updateSubscriptionGroupConfig(groupConfig); + + this.sendClientSettings(stub, buildPushConsumerClientSettings(group)).get(); + receiveMessage(blockingStub, topic, group, 1); + + String messageGroup = "group"; + this.sendClientSettings(stub, buildProducerClientSettings(topic)).get(); + List messageIdList = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + String messageId = createUniqID(); + messageIdList.add(messageId); + SendMessageResponse sendResponse = blockingStub.sendMessage(buildSendOrderMessageRequest(topic, messageId, messageGroup)); + assertSendMessage(sendResponse, messageId); + } + + List messageRecvList = new ArrayList<>(); + this.sendClientSettings(stub, buildPushConsumerClientSettings(group)).get(); + await().atMost(java.time.Duration.ofSeconds(20)).until(() -> { + List retryMessageList = getMessageFromReceiveMessageResponse(receiveMessage(blockingStub, topic, group)); + if (retryMessageList.isEmpty()) { + return false; + } + for (Message message : retryMessageList) { + String messageId = message.getSystemProperties().getMessageId(); + messageRecvList.add(messageId); + blockingStub.ackMessage(buildAckMessageRequest(topic, group, + AckMessageEntry.newBuilder().setMessageId(messageId).setReceiptHandle(message.getSystemProperties().getReceiptHandle()).build())); + } + return messageRecvList.size() == messageIdList.size(); + }); + + for (int i = 0; i < messageIdList.size(); i++) { + assertThat(messageRecvList.get(i)).isEqualTo(messageIdList.get(i)); + } + } + + public List receiveMessage(MessagingServiceGrpc.MessagingServiceBlockingStub stub, + String topic, String group) { + return receiveMessage(stub, topic, group, 15); + } + + public List receiveMessage(MessagingServiceGrpc.MessagingServiceBlockingStub stub, + String topic, String group, int timeSeconds) { + List responseList = new ArrayList<>(); + Iterator responseIterator = stub.withDeadlineAfter(timeSeconds, TimeUnit.SECONDS) + .receiveMessage(buildReceiveMessageRequest(topic, group)); + while (responseIterator.hasNext()) { + responseList.add(responseIterator.next()); + } + return responseList; + } + + public List getMessageFromReceiveMessageResponse(List responseList) { + List messageList = new ArrayList<>(); + for (ReceiveMessageResponse response : responseList) { + if (response.hasMessage()) { + messageList.add(response.getMessage()); + } + } + return messageList; + } + + public QueryRouteRequest buildQueryRouteRequest(String topic) { + return QueryRouteRequest.newBuilder() + .setEndpoints(buildEndpoints(port)) + .setTopic(Resource.newBuilder() + .setName(topic) + .build()) + .build(); + } + + public QueryAssignmentRequest buildQueryAssignmentRequest(String topic, String group) { + return QueryAssignmentRequest.newBuilder() + .setEndpoints(buildEndpoints(port)) + .setTopic(Resource.newBuilder().setName(topic).build()) + .setGroup(Resource.newBuilder().setName(group).build()) + .build(); + } + + public SendMessageRequest buildSendMessageRequest(String topic, String messageId) { + return SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(topic) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId(messageId) + .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(); + } + + public SendMessageRequest buildSendOrderMessageRequest(String topic, String messageId, String messageGroup) { + return SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(topic) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId(messageId) + .setQueueId(0) + .setMessageType(MessageType.FIFO) + .setMessageGroup(messageGroup) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .setBody(ByteString.copyFromUtf8("123")) + .build()) + .build(); + } + + public SendMessageRequest buildSendBigMessageRequest(String topic, String messageId, int messageSize) { + return SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(topic) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId(messageId) + .setQueueId(0) + .setMessageType(MessageType.NORMAL) + .setBodyEncoding(Encoding.GZIP) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .setBody(ByteString.copyFromUtf8(RandomUtils.getStringWithCharacter(messageSize))) + .build()) + .build(); + } + + public SendMessageRequest buildTransactionSendMessageRequest(String topic, String messageId) { + return SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(topic) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId(messageId) + .setQueueId(0) + .setMessageType(MessageType.TRANSACTION) + .setOrphanedTransactionRecoveryDuration(Duration.newBuilder().setSeconds(10)) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .setBody(ByteString.copyFromUtf8("123")) + .build()) + .build(); + } + + public ReceiveMessageRequest buildReceiveMessageRequest(String topic, String group) { + return ReceiveMessageRequest.newBuilder() + .setGroup(Resource.newBuilder() + .setName(group) + .build()) + .setMessageQueue(MessageQueue.newBuilder() + .setTopic(Resource.newBuilder() + .setName(topic) + .build()) + .setId(-1) + .build()) + .setBatchSize(1) + .setAutoRenew(false) + .setInvisibleDuration(Duration.newBuilder() + .setSeconds(3) + .build()) + .build(); + } + + public AckMessageRequest buildAckMessageRequest(String topic, String group, AckMessageEntry... entry) { + return AckMessageRequest.newBuilder() + .setGroup(Resource.newBuilder() + .setName(group) + .build()) + .setTopic(Resource.newBuilder() + .setName(topic) + .build()) + .addAllEntries(Arrays.stream(entry).collect(Collectors.toList())) + .build(); + } + + public EndTransactionRequest buildEndTransactionRequest(String topic, String messageId, String transactionId, + TransactionResolution resolution) { + return EndTransactionRequest.newBuilder() + .setMessageId(messageId) + .setTopic(Resource.newBuilder() + .setName(topic) + .build()) + .setTransactionId(transactionId) + .setResolution(resolution) + .setSource(TransactionSource.SOURCE_SERVER_CHECK) + .build(); + } + + public ChangeInvisibleDurationRequest buildChangeInvisibleDurationRequest(String topic, String group, + String receiptHandle, int second) { + return ChangeInvisibleDurationRequest.newBuilder() + .setTopic(Resource.newBuilder().setName(topic).build()) + .setGroup(Resource.newBuilder().setName(group).build()) + .setInvisibleDuration(Durations.fromSeconds(second)) + .setReceiptHandle(receiptHandle) + .build(); + } + + public void assertQueryRoute(QueryRouteResponse response, int messageQueueSize) { + assertThat(response.getStatus()).isEqualTo(ResponseBuilder.getInstance().buildStatus(Code.OK, Code.OK.name())); + assertThat(response.getMessageQueuesList().size()).isEqualTo(messageQueueSize); + assertThat(response.getMessageQueues(0).getBroker().getEndpoints().getAddresses(0).getPort()).isEqualTo(ConfigurationManager.getProxyConfig().getGrpcServerPort()); + } + + public void assertQueryAssignment(QueryAssignmentResponse response, int assignmentCount) { + assertThat(response.getStatus()).isEqualTo(ResponseBuilder.getInstance().buildStatus(Code.OK, Code.OK.name())); + assertThat(response.getAssignmentsCount()).isEqualTo(assignmentCount); + assertThat(response.getAssignments(0).getMessageQueue().getBroker().getEndpoints().getAddresses(0).getPort()).isEqualTo(ConfigurationManager.getProxyConfig().getGrpcServerPort()); + } + + public void assertSendMessage(SendMessageResponse response, String messageId) { + assertThat(response.getStatus()).isEqualTo(ResponseBuilder.getInstance().buildStatus(Code.OK, Code.OK.name())); + assertThat(response.getEntries(0).getMessageId()).isEqualTo(messageId); + } + + public Message assertAndGetReceiveMessage(List response, String messageId) { + assertThat(response.get(0).hasStatus()).isTrue(); + assertThat(response.get(0).getStatus() + .getCode()).isEqualTo(Code.OK); + assertThat(response.get(1).getMessage() + .getSystemProperties() + .getMessageId()).isEqualTo(messageId); + return response.get(1).getMessage(); + } + + public void assertRecoverOrphanedTransactionCommand(RecoverOrphanedTransactionCommand command, String messageId) { + assertThat(command.getTransactionId()).isNotBlank(); + } + + public void assertEndTransactionResponse(EndTransactionResponse response) { + assertThat(response.getStatus().getCode()).isEqualTo(Code.OK); + } + + public void assertChangeInvisibleDurationResponse(ChangeInvisibleDurationResponse response, String prevHandle) { + assertThat(response.getStatus().getCode()).isEqualTo(Code.OK); + assertThat(response.getReceiptHandle()).isNotEqualTo(prevHandle); + } + + public Endpoints buildEndpoints(int port) { + return Endpoints.newBuilder() + .setScheme(AddressScheme.IPv4) + .addAddresses(Address.newBuilder() + .setHost("127.0.0.1") + .setPort(port) + .build()) + .build(); + } + + public Settings buildSimpleConsumerClientSettings(String group) { + return Settings.newBuilder() + .setClientType(ClientType.SIMPLE_CONSUMER) + .setRequestTimeout(Durations.fromSeconds(3)) + .setSubscription(Subscription.newBuilder() + .setGroup(Resource.newBuilder().setName(group).build()) + .build()) + .build(); + } + + public Settings buildPushConsumerClientSettings(String group) { + return buildPushConsumerClientSettings(2, group); + } + + public Settings buildPushConsumerClientSettings(int maxDeliveryAttempts, String group) { + return Settings.newBuilder() + .setClientType(ClientType.PUSH_CONSUMER) + .setRequestTimeout(Durations.fromSeconds(3)) + .setBackoffPolicy(RetryPolicy.newBuilder() + .setMaxAttempts(maxDeliveryAttempts) + .build()) + .setSubscription(Subscription.newBuilder() + .setGroup(Resource.newBuilder().setName(group).build()) + .build()) + .build(); + } + + public Settings buildProducerClientSettings(String... topics) { + List topicResources = Arrays.stream(topics).map(topic -> Resource.newBuilder().setName(topic).build()) + .collect(Collectors.toList()); + return Settings.newBuilder() + .setClientType(ClientType.PRODUCER) + .setPublishing(Publishing.newBuilder() + .addAllTopics(topicResources) + .build()) + .build(); + } + + protected static class DefaultTelemetryCommandStreamObserver implements StreamObserver { + + @Override + public void onNext(TelemetryCommand value) { + + } + + @Override + public void onError(Throwable t) { + + } + + @Override + public void onCompleted() { + + } + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/grpc/v2/LocalGrpcIT.java b/test/src/test/java/org/apache/rocketmq/test/grpc/v2/LocalGrpcIT.java new file mode 100644 index 0000000..5dd06f5 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/grpc/v2/LocalGrpcIT.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.test.grpc.v2; + +import apache.rocketmq.v2.QueryRouteResponse; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.grpc.v2.GrpcMessagingApplication; +import org.apache.rocketmq.proxy.processor.DefaultMessagingProcessor; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.junit.After; +import org.junit.Before; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.Ignore; +import org.junit.runners.MethodSorters; + +@FixMethodOrder(value = MethodSorters.NAME_ASCENDING) +public class LocalGrpcIT extends GrpcBaseIT { + + private MessagingProcessor messagingProcessor; + private GrpcMessagingApplication grpcMessagingApplication; + + @Before + public void setUp() throws Exception { + super.setUp(); + messagingProcessor = DefaultMessagingProcessor.createForLocalMode(brokerController1); + messagingProcessor.start(); + grpcMessagingApplication = GrpcMessagingApplication.create(messagingProcessor); + grpcMessagingApplication.start(); + setUpServer(grpcMessagingApplication, ConfigurationManager.getProxyConfig().getGrpcServerPort(), true); + } + + @After + public void clean() throws Exception { + messagingProcessor.shutdown(); + grpcMessagingApplication.shutdown(); + shutdown(); + } + + @Test + public void testQueryRoute() throws Exception { + String topic = initTopic(); + + QueryRouteResponse response = blockingStub.queryRoute(buildQueryRouteRequest(topic)); + assertQueryRoute(response, brokerControllerList.size() * DEFAULT_QUEUE_NUMS); + } + + @Test + public void testQueryAssignment() throws Exception { + super.testQueryAssignment(); + } + + @Test + public void testQueryFifoAssignment() throws Exception { + super.testQueryFifoAssignment(); + } + + @Test + public void testTransactionCheckThenCommit() { + super.testTransactionCheckThenCommit(); + } + + @Test + @Ignore + public void testSimpleConsumerSendAndRecvDelayMessage() throws Exception { + super.testSimpleConsumerSendAndRecvDelayMessage(); + } + + @Test + public void testSimpleConsumerSendAndRecallDelayMessage() throws Exception { + super.testSimpleConsumerSendAndRecallDelayMessage(); + } + + @Test + public void testSimpleConsumerSendAndRecvBigMessage() throws Exception { + super.testSimpleConsumerSendAndRecvBigMessage(); + } + + @Test + public void testSimpleConsumerSendAndRecv() throws Exception { + super.testSimpleConsumerSendAndRecv(); + } + + @Test + public void testSimpleConsumerToDLQ() throws Exception { + super.testSimpleConsumerToDLQ(); + } + + @Test + public void testConsumeOrderly() throws Exception { + super.testConsumeOrderly(); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/lmq/TestBenchLmqStore.java b/test/src/test/java/org/apache/rocketmq/test/lmq/TestBenchLmqStore.java new file mode 100644 index 0000000..cb35b39 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/lmq/TestBenchLmqStore.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.test.lmq; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.client.consumer.PullCallback; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.impl.MQClientAPIImpl; +import org.apache.rocketmq.client.impl.consumer.DefaultMQPullConsumerImpl; +import org.apache.rocketmq.client.impl.consumer.RebalanceImpl; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.apache.rocketmq.test.lmq.benchmark.BenchLmqStore; +import org.junit.Test; +import org.junit.runner.RunWith; +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.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TestBenchLmqStore { + @Test + public void test() throws MQBrokerException, RemotingException, InterruptedException, MQClientException { + System.setProperty("sendThreadNum", "1"); + System.setProperty("pullConsumerNum", "1"); + System.setProperty("consumerThreadNum", "1"); + BenchLmqStore.defaultMQProducer = mock(DefaultMQProducer.class); + SendResult sendResult = new SendResult(); + when(BenchLmqStore.defaultMQProducer.send(any(Message.class))).thenReturn(sendResult); + BenchLmqStore.doSend(); + Thread.sleep(100L); + //verify(BenchLmqStore.defaultMQProducer, atLeastOnce()).send(any(Message.class)); + BenchLmqStore.defaultMQPullConsumers = new DefaultMQPullConsumer[1]; + BenchLmqStore.defaultMQPullConsumers[0] = mock(DefaultMQPullConsumer.class); + BenchLmqStore.doPull(new ConcurrentHashMap<>(), new MessageQueue(), 1L); + verify(BenchLmqStore.defaultMQPullConsumers[0], atLeastOnce()).pullBlockIfNotFound(any(MessageQueue.class), anyString(), anyLong(), anyInt(), any( + PullCallback.class)); + } + + @Test + public void testOffset() throws RemotingException, InterruptedException, MQClientException, MQBrokerException, IllegalAccessException { + System.setProperty("sendThreadNum", "1"); + DefaultMQPullConsumer defaultMQPullConsumer = mock(DefaultMQPullConsumer.class); + BenchLmqStore.defaultMQPullConsumers = new DefaultMQPullConsumer[1]; + BenchLmqStore.defaultMQPullConsumers[0] = defaultMQPullConsumer; + DefaultMQPullConsumerImpl defaultMQPullConsumerImpl = mock(DefaultMQPullConsumerImpl.class); + when(defaultMQPullConsumer.getDefaultMQPullConsumerImpl()).thenReturn(defaultMQPullConsumerImpl); + RebalanceImpl rebalanceImpl = mock(RebalanceImpl.class); + when(defaultMQPullConsumerImpl.getRebalanceImpl()).thenReturn(rebalanceImpl); + MQClientInstance mqClientInstance = mock(MQClientInstance.class); + when(rebalanceImpl.getmQClientFactory()).thenReturn(mqClientInstance); + MQClientAPIImpl mqClientAPI = mock(MQClientAPIImpl.class); + when(mqClientInstance.getMQClientAPIImpl()).thenReturn(mqClientAPI); + TopicRouteData topicRouteData = new TopicRouteData(); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(MixAll.MASTER_ID, "test"); + List brokerData = Collections.singletonList(new BrokerData("test", "test", brokerAddrs)); + topicRouteData.setBrokerDatas(brokerData); + FieldUtils.writeStaticField(BenchLmqStore.class, "lmqTopic", "test", true); + when(mqClientAPI.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(topicRouteData); + BenchLmqStore.doBenchOffset(); + Thread.sleep(100L); + verify(mqClientAPI, atLeastOnce()).queryConsumerOffset(anyString(), any(QueryConsumerOffsetRequestHeader.class), anyLong()); + verify(mqClientAPI, atLeastOnce()).updateConsumerOffset(anyString(), any(UpdateConsumerOffsetRequestHeader.class), anyLong()); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/offset/LagCalculationIT.java b/test/src/test/java/org/apache/rocketmq/test/offset/LagCalculationIT.java new file mode 100644 index 0000000..bfed96e --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/offset/LagCalculationIT.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.test.offset; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.filter.ConsumerFilterData; +import org.apache.rocketmq.broker.filter.ExpressionMessageFilter; +import org.apache.rocketmq.client.consumer.MessageSelector; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.filter.ExpressionType; +import org.apache.rocketmq.common.message.Message; +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.admin.ConsumeStats; +import org.apache.rocketmq.remoting.protocol.admin.OffsetWrapper; +import org.apache.rocketmq.remoting.protocol.filter.FilterAPI; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.store.DefaultMessageFilter; +import org.apache.rocketmq.store.exception.ConsumeQueueException; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.client.rmq.RMQSqlConsumer; +import org.apache.rocketmq.test.factory.ConsumerFactory; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQBlockListener; +import org.apache.rocketmq.test.message.MessageQueueMsg; +import org.apache.rocketmq.test.util.MQAdminTestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; + +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; + +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class LagCalculationIT extends BaseConf { + private static final Logger LOGGER = LoggerFactory.getLogger(LagCalculationIT.class); + private RMQNormalProducer producer = null; + private RMQNormalConsumer consumer = null; + private String topic = null; + private RMQBlockListener blockListener = null; + + @Before + public void setUp() { + topic = initTopic(); + LOGGER.info(String.format("use topic: %s;", topic)); + for (BrokerController controller : brokerControllerList) { + controller.getBrokerConfig().setLongPollingEnable(false); + controller.getBrokerConfig().setShortPollingTimeMills(500); + controller.getBrokerConfig().setEstimateAccumulation(true); + } + producer = getProducer(NAMESRV_ADDR, topic); + blockListener = new RMQBlockListener(false); + consumer = getConsumer(NAMESRV_ADDR, topic, "*", blockListener); + } + + @After + public void tearDown() { + shutdown(); + } + + private Pair getLag(List mqs) throws ConsumeQueueException { + long lag = 0; + long pullLag = 0; + for (BrokerController controller : brokerControllerList) { + ConsumeStats consumeStats = MQAdminTestUtils.examineConsumeStats(controller.getBrokerAddr(), topic, consumer.getConsumerGroup()); + Map offsetTable = consumeStats.getOffsetTable(); + for (MessageQueue mq : mqs) { + if (mq.getBrokerName().equals(controller.getBrokerConfig().getBrokerName())) { + long brokerOffset = controller.getMessageStore().getMaxOffsetInQueue(topic, mq.getQueueId()); + + long consumerOffset = controller.getConsumerOffsetManager().queryOffset(consumer.getConsumerGroup(), + topic, mq.getQueueId()); + long pullOffset = + controller.getConsumerOffsetManager().queryPullOffset(consumer.getConsumerGroup(), + topic, mq.getQueueId()); + OffsetWrapper offsetWrapper = offsetTable.get(mq); + assertEquals(brokerOffset, offsetWrapper.getBrokerOffset()); + if (offsetWrapper.getConsumerOffset() != consumerOffset || offsetWrapper.getPullOffset() != pullOffset) { + return new Pair<>(-1L, -1L); + } + lag += brokerOffset - consumerOffset; + pullLag += brokerOffset - pullOffset; + } + } + } + return new Pair<>(lag, pullLag); + } + + public void waitForFullyDispatched() { + await().atMost(5, TimeUnit.SECONDS).until(() -> { + for (BrokerController controller : brokerControllerList) { + if (controller.getMessageStore().dispatchBehindBytes() != 0) { + return false; + } + } + return true; + }); + } + + @Test + public void testCalculateLag() throws ConsumeQueueException { + int msgSize = 10; + List mqs = producer.getMessageQueue(); + MessageQueueMsg mqMsgs = new MessageQueueMsg(mqs, msgSize); + + producer.send(mqMsgs.getMsgsWithMQ()); + waitForFullyDispatched(); + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + consumer.getConsumer().getDefaultMQPushConsumerImpl().persistConsumerOffset(); + + // wait for consume all msgs + await().atMost(5, TimeUnit.SECONDS).until(() -> { + Pair lag = getLag(mqs); + return lag.getObject1() == 0 && lag.getObject2() == 0; + }); + + blockListener.setBlock(true); + consumer.clearMsg(); + producer.clearMsg(); + producer.send(mqMsgs.getMsgsWithMQ()); + waitForFullyDispatched(); + + // wait for pull all msgs + await().atMost(5, TimeUnit.SECONDS).until(() -> { + Pair lag = getLag(mqs); + return lag.getObject1() == producer.getAllMsgBody().size() && lag.getObject2() == 0; + }); + + blockListener.setBlock(false); + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + consumer.shutdown(); + producer.clearMsg(); + producer.send(mqMsgs.getMsgsWithMQ()); + waitForFullyDispatched(); + + Pair lag = getLag(mqs); + assertEquals(producer.getAllMsgBody().size(), (long) lag.getObject1()); + assertEquals(producer.getAllMsgBody().size(), (long) lag.getObject2()); + } + + @Test + public void testEstimateLag() throws Exception { + int msgNoTagSize = 80; + int msgWithTagSize = 20; + int repeat = 2; + String tag = "TAG_FOR_TEST_ESTIMATE"; + String sql = "TAGS = 'TAG_FOR_TEST_ESTIMATE' And value < " + repeat / 2; + MessageSelector selector = MessageSelector.bySql(sql); + RMQBlockListener sqlListener = new RMQBlockListener(true); + RMQSqlConsumer sqlConsumer = ConsumerFactory.getRMQSqlConsumer(NAMESRV_ADDR, initConsumerGroup(), topic, selector, sqlListener); + RMQBlockListener tagListener = new RMQBlockListener(true); + RMQNormalConsumer tagConsumer = getConsumer(NAMESRV_ADDR, topic, tag, tagListener); + + //init subscriptionData & consumerFilterData for sql + SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(topic, sql, ExpressionType.SQL92); + for (BrokerController controller : brokerControllerList) { + controller.getConsumerFilterManager().register(topic, sqlConsumer.getConsumerGroup(), sql, ExpressionType.SQL92, subscriptionData.getSubVersion()); + } + + // wait for building filter data + await().atMost(5, TimeUnit.SECONDS).until(() -> sqlListener.isBlocked() && tagListener.isBlocked()); + + List mqs = producer.getMessageQueue(); + for (int i = 0; i < repeat; i++) { + MessageQueueMsg mqMsgs = new MessageQueueMsg(mqs, msgNoTagSize); + Map> msgMap = mqMsgs.getMsgsWithMQ(); + mqMsgs = new MessageQueueMsg(mqs, msgWithTagSize, tag); + Map> msgWithTagMap = mqMsgs.getMsgsWithMQ(); + int finalI = i; + msgMap.forEach((mq, msgList) -> { + List msgWithTagList = msgWithTagMap.get(mq); + for (Object o : msgWithTagList) { + ((Message) o).putUserProperty("value", String.valueOf(finalI)); + } + msgList.addAll(msgWithTagList); + Collections.shuffle(msgList); + }); + producer.send(msgMap); + } + + // test lag estimation for tag consumer + for (BrokerController controller : brokerControllerList) { + for (MessageQueue mq : mqs) { + if (mq.getBrokerName().equals(controller.getBrokerConfig().getBrokerName())) { + long brokerOffset = controller.getMessageStore().getMaxOffsetInQueue(topic, mq.getQueueId()); + long estimateMessageCount = controller.getMessageStore() + .estimateMessageCount(topic, mq.getQueueId(), 0, brokerOffset, + new DefaultMessageFilter(FilterAPI.buildSubscriptionData(topic, tag))); + assertEquals(repeat * msgWithTagSize, estimateMessageCount); + } + } + } + + // test lag estimation for sql consumer + for (BrokerController controller : brokerControllerList) { + for (MessageQueue mq : mqs) { + if (mq.getBrokerName().equals(controller.getBrokerConfig().getBrokerName())) { + long brokerOffset = controller.getMessageStore().getMaxOffsetInQueue(topic, mq.getQueueId()); + ConsumerFilterData consumerFilterData = controller.getConsumerFilterManager().get(topic, sqlConsumer.getConsumerGroup()); + long estimateMessageCount = controller.getMessageStore() + .estimateMessageCount(topic, mq.getQueueId(), 0, brokerOffset, + new ExpressionMessageFilter(subscriptionData, consumerFilterData, controller.getConsumerFilterManager())); + assertEquals(repeat / 2 * msgWithTagSize, estimateMessageCount); + } + } + } + + sqlConsumer.shutdown(); + tagConsumer.shutdown(); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/offset/OffsetNotFoundIT.java b/test/src/test/java/org/apache/rocketmq/test/offset/OffsetNotFoundIT.java new file mode 100644 index 0000000..1c96aa4 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/offset/OffsetNotFoundIT.java @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.test.offset; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class OffsetNotFoundIT extends BaseConf { + + private OffsetRpcHook offsetRpcHook = new OffsetRpcHook(); + + static class OffsetRpcHook implements RPCHook { + + private boolean throwException = false; + + private boolean addSetZeroOfNotFound = false; + + @Override + public void doBeforeRequest(String remoteAddr, RemotingCommand request) { + + if (request.getCode() == RequestCode.QUERY_CONSUMER_OFFSET) { + if (throwException) { + throw new RuntimeException("Stop by rpc hook"); + } + if (addSetZeroOfNotFound) { + request.getExtFields().put("setZeroIfNotFound", "false"); + } + } + } + + @Override + public void doAfterResponse(String remoteAddr, RemotingCommand request, + RemotingCommand response) { + + } + } + + @Before + public void setUp() { + for (BrokerController brokerController: brokerControllerList) { + brokerController.registerServerRPCHook(offsetRpcHook); + } + + + } + + @After + public void tearDown() { + super.shutdown(); + } + + @Test + public void testConsumeStopAndResume() { + String topic = initTopic(); + RMQNormalProducer producer = getProducer(NAMESRV_ADDR, topic); + int msgSize = 10; + producer.send(msgSize); + Assert.assertEquals("Not all sent succeeded", msgSize, producer.getAllUndupMsgBody().size()); + try { + offsetRpcHook.throwException = true; + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), 15000); + Assert.assertEquals(0, consumer.getListener().getAllMsgBody().size()); + consumer.shutdown(); + } finally { + offsetRpcHook.throwException = false; + } + //test the normal + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), 15000); + Assert.assertEquals(producer.getAllMsgBody().size(), consumer.getListener().getAllMsgBody().size()); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + consumer.shutdown(); + } + + + @Test + public void testOffsetNotFoundException() { + String topic = initTopic(); + String group = initConsumerGroup(); + RMQNormalProducer producer = getProducer(NAMESRV_ADDR, topic); + int msgSize = 10; + producer.send(msgSize); + Assert.assertEquals("Not all sent succeeded", msgSize, producer.getAllUndupMsgBody().size()); + try { + offsetRpcHook.addSetZeroOfNotFound = true; + //test the normal + RMQNormalConsumer consumer = new RMQNormalConsumer(NAMESRV_ADDR, topic, "*", group, new RMQNormalListener()); + consumer.create(false); + consumer.getConsumer().setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + consumer.start(); + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), 15000); + Assert.assertEquals(producer.getAllMsgBody().size(), consumer.getListener().getAllMsgBody().size()); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + consumer.shutdown(); + } finally { + offsetRpcHook.addSetZeroOfNotFound = false; + } + + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/offset/OffsetResetForPopIT.java b/test/src/test/java/org/apache/rocketmq/test/offset/OffsetResetForPopIT.java new file mode 100644 index 0000000..b9798cf --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/offset/OffsetResetForPopIT.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.test.offset; + +import com.google.common.collect.Lists; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.rocketmq.client.consumer.PopResult; +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.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.admin.ConsumeStats; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.client.rmq.RMQPopConsumer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.MQAdminTestUtils; +import org.apache.rocketmq.test.util.MQRandomUtils; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static org.awaitility.Awaitility.await; + +public class OffsetResetForPopIT extends BaseConf { + + private static final Logger LOGGER = LoggerFactory.getLogger(OffsetResetForPopIT.class); + + private String topic; + private String group; + private RMQNormalProducer producer = null; + private RMQPopConsumer consumer = null; + private DefaultMQAdminExt adminExt; + + @Before + public void setUp() throws Exception { + // reset pop offset rely on server side offset + brokerController1.getBrokerConfig().setUseServerSideResetOffset(true); + + adminExt = BaseConf.getAdmin(NAMESRV_ADDR); + adminExt.start(); + + topic = MQRandomUtils.getRandomTopic(); + this.createAndWaitTopicRegister(BROKER1_NAME, topic); + group = initConsumerGroup(); + LOGGER.info(String.format("use topic: %s, group: %s", topic, group)); + producer = getProducer(NAMESRV_ADDR, topic); + } + + @After + public void tearDown() { + shutdown(); + } + + private void createAndWaitTopicRegister(String brokerName, String topic) throws Exception { + String brokerAddress = CommandUtil.fetchMasterAddrByBrokerName(adminExt, brokerName); + TopicConfig topicConfig = new TopicConfig(topic); + topicConfig.setReadQueueNums(1); + topicConfig.setWriteQueueNums(1); + adminExt.createAndUpdateTopicConfig(brokerAddress, topicConfig); + + await().atMost(30, TimeUnit.SECONDS).until( + () -> MQAdminTestUtils.checkTopicExist(adminExt, topic)); + } + + private void resetOffsetInner(long resetOffset) { + try { + // reset offset by queue + adminExt.resetOffsetByQueueId(brokerController1.getBrokerAddr(), + consumer.getConsumerGroup(), consumer.getTopic(), 0, resetOffset); + } catch (Exception ignore) { + } + } + + private void ackMessageSync(MessageExt messageExt) { + try { + consumer.ackAsync(brokerController1.getBrokerAddr(), + messageExt.getProperty(MessageConst.PROPERTY_POP_CK)).get(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void ackMessageSync(List messageExtList) { + if (messageExtList != null) { + messageExtList.forEach(this::ackMessageSync); + } + } + + @Test + public void testResetOffsetAfterPop() throws Exception { + int messageCount = 10; + int resetOffset = 4; + producer.send(messageCount); + consumer = new RMQPopConsumer(NAMESRV_ADDR, topic, "*", group, new RMQNormalListener()); + consumer.start(); + + MessageQueue mq = new MessageQueue(topic, BROKER1_NAME, 0); + PopResult popResult = consumer.pop(brokerController1.getBrokerAddr(), mq); + Assert.assertEquals(10, popResult.getMsgFoundList().size()); + + resetOffsetInner(resetOffset); + popResult = consumer.pop(brokerController1.getBrokerAddr(), mq); + Assert.assertTrue(popResult != null && popResult.getMsgFoundList() != null); + Assert.assertEquals(messageCount - resetOffset, popResult.getMsgFoundList().size()); + } + + @Test + public void testResetOffsetThenAckOldForPopOrderly() throws Exception { + int messageCount = 10; + int resetOffset = 2; + producer.send(messageCount); + consumer = new RMQPopConsumer(NAMESRV_ADDR, topic, "*", group, new RMQNormalListener()); + consumer.start(); + + MessageQueue mq = new MessageQueue(topic, BROKER1_NAME, 0); + PopResult popResult1 = consumer.popOrderly(brokerController1.getBrokerAddr(), mq); + Assert.assertEquals(10, popResult1.getMsgFoundList().size()); + + resetOffsetInner(resetOffset); + ConsumeStats consumeStats = adminExt.examineConsumeStats(group, topic); + Assert.assertEquals(resetOffset, consumeStats.getOffsetTable().get(mq).getConsumerOffset()); + + PopResult popResult2 = consumer.popOrderly(brokerController1.getBrokerAddr(), mq); + Assert.assertTrue(popResult2 != null && popResult2.getMsgFoundList() != null); + Assert.assertEquals(messageCount - resetOffset, popResult2.getMsgFoundList().size()); + + // ack old msg, expect has no effect + ackMessageSync(popResult1.getMsgFoundList()); + Assert.assertTrue(brokerController1.getConsumerOrderInfoManager() + .checkBlock(null, topic, group, 0, RMQPopConsumer.DEFAULT_INVISIBLE_TIME)); + + // ack new msg + ackMessageSync(popResult2.getMsgFoundList()); + Assert.assertFalse(brokerController1.getConsumerOrderInfoManager() + .checkBlock(null, topic, group, 0, RMQPopConsumer.DEFAULT_INVISIBLE_TIME)); + } + + @Test + public void testRestOffsetToSkipMsgForPopOrderly() throws Exception { + int messageCount = 10; + int resetOffset = 4; + producer.send(messageCount); + consumer = new RMQPopConsumer(NAMESRV_ADDR, topic, "*", group, new RMQNormalListener()); + resetOffsetInner(resetOffset); + consumer.start(); + + MessageQueue mq = new MessageQueue(topic, BROKER1_NAME, 0); + PopResult popResult = consumer.popOrderly(brokerController1.getBrokerAddr(), mq); + Assert.assertEquals(messageCount - resetOffset, popResult.getMsgFoundList().size()); + Assert.assertTrue(brokerController1.getConsumerOrderInfoManager() + .checkBlock(null, topic, group, 0, RMQPopConsumer.DEFAULT_INVISIBLE_TIME)); + + ackMessageSync(popResult.getMsgFoundList()); + TimeUnit.SECONDS.sleep(1); + Assert.assertFalse(brokerController1.getConsumerOrderInfoManager() + .checkBlock(null, topic, group, 0, RMQPopConsumer.DEFAULT_INVISIBLE_TIME)); + } + + @Test + public void testResetOffsetAfterPopWhenOpenBufferAndWait() throws Exception { + int messageCount = 10; + int resetOffset = 4; + brokerController1.getBrokerConfig().setEnablePopBufferMerge(true); + producer.send(messageCount); + consumer = new RMQPopConsumer(NAMESRV_ADDR, topic, "*", group, new RMQNormalListener()); + consumer.start(); + + MessageQueue mq = new MessageQueue(topic, BROKER1_NAME, 0); + PopResult popResult = consumer.pop(brokerController1.getBrokerAddr(), mq); + Assert.assertEquals(10, popResult.getMsgFoundList().size()); + + resetOffsetInner(resetOffset); + TimeUnit.MILLISECONDS.sleep(brokerController1.getBrokerConfig().getPopCkStayBufferTimeOut()); + + popResult = consumer.pop(brokerController1.getBrokerAddr(), mq); + Assert.assertTrue(popResult != null && popResult.getMsgFoundList() != null); + Assert.assertEquals(messageCount - resetOffset, popResult.getMsgFoundList().size()); + } + + @Test + public void testResetOffsetWhilePopWhenOpenBuffer() { + testResetOffsetWhilePop(8, false, false, 5); + } + + @Test + public void testResetOffsetWhilePopWhenOpenBufferAndAck() { + testResetOffsetWhilePop(8, false, true, 5); + } + + @Test + public void testMultipleResetOffsetWhilePopWhenOpenBufferAndAck() { + testResetOffsetWhilePop(8, false, true, 3, 5); + } + + @Test + public void testResetFutureOffsetWhilePopWhenOpenBufferAndAck() { + testResetOffsetWhilePop(2, true, true, 8); + } + + @Test + public void testMultipleResetFutureOffsetWhilePopWhenOpenBufferAndAck() { + testResetOffsetWhilePop(2, true, true, 5, 8); + } + + private void testResetOffsetWhilePop(int targetCount, boolean resetFuture, boolean needAck, + int... resetOffset) { + brokerController1.getBrokerConfig().setEnablePopBufferMerge(true); + producer.send(10); + + // max pop one message per request + consumer = + new RMQPopConsumer(NAMESRV_ADDR, topic, "*", group, new RMQNormalListener(), 1); + + MessageQueue mq = new MessageQueue(topic, BROKER1_NAME, 0); + AtomicInteger counter = new AtomicInteger(0); + consumer.start(); + Executors.newSingleThreadScheduledExecutor().execute(() -> { + long start = System.currentTimeMillis(); + while (System.currentTimeMillis() - start <= 30 * 1000L) { + try { + PopResult popResult = consumer.pop(brokerController1.getBrokerAddr(), mq); + if (popResult == null || popResult.getMsgFoundList() == null) { + continue; + } + + int count = counter.addAndGet(popResult.getMsgFoundList().size()); + if (needAck) { + ackMessageSync(popResult.getMsgFoundList()); + } + if (count == targetCount) { + for (int offset : resetOffset) { + resetOffsetInner(offset); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + + await().atMost(10, TimeUnit.SECONDS).until(() -> { + boolean result = true; + if (resetFuture) { + result = counter.get() < 10; + } + result &= counter.get() >= targetCount + 10 - resetOffset[resetOffset.length - 1]; + return result; + }); + } + + @Test + public void testResetFutureOffsetWhilePopOrderlyAndAck() { + testResetOffsetWhilePopOrderly(1, + Lists.newArrayList(0, 5, 6, 7, 8, 9), Lists.newArrayList(5), 6); + } + + @Test + public void testMultipleResetFutureOffsetWhilePopOrderlyAndAck() { + testResetOffsetWhilePopOrderly(1, + Lists.newArrayList(0, 5, 6, 7, 8, 9), Lists.newArrayList(3, 5), 6); + } + + @Test + public void testResetOffsetWhilePopOrderlyAndAck() { + testResetOffsetWhilePopOrderly(5, + Lists.newArrayList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), + Lists.newArrayList(3), 12); + } + + @Test + public void testMultipleResetOffsetWhilePopOrderlyAndAck() { + testResetOffsetWhilePopOrderly(5, + Lists.newArrayList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), + Lists.newArrayList(3, 1), 14); + } + + private void testResetOffsetWhilePopOrderly(int targetCount, List expectMsgReceive, + List resetOffset, int expectCount) { + brokerController1.getBrokerConfig().setEnablePopBufferMerge(true); + for (int i = 0; i < 10; i++) { + Message msg = new Message(topic, (String.valueOf(i)).getBytes()); + producer.send(msg); + } + consumer = new RMQPopConsumer(NAMESRV_ADDR, topic, "*", group, new RMQNormalListener(), 1); + MessageQueue mq = new MessageQueue(topic, BROKER1_NAME, 0); + Set msgReceive = Collections.newSetFromMap(new ConcurrentHashMap<>()); + AtomicInteger counter = new AtomicInteger(0); + consumer.start(); + + Executors.newSingleThreadScheduledExecutor().execute(() -> { + long start = System.currentTimeMillis(); + while (System.currentTimeMillis() - start <= 30 * 1000L) { + try { + PopResult popResult = consumer.popOrderly(brokerController1.getBrokerAddr(), mq); + if (popResult == null || popResult.getMsgFoundList() == null) { + continue; + } + int count = counter.addAndGet(popResult.getMsgFoundList().size()); + for (MessageExt messageExt : popResult.getMsgFoundList()) { + msgReceive.add(Integer.valueOf(new String(messageExt.getBody()))); + ackMessageSync(messageExt); + } + if (count == targetCount) { + for (int offset : resetOffset) { + resetOffsetInner(offset); + } + } + } catch (Exception e) { + // do nothing; + } + } + }); + + await().atMost(10, TimeUnit.SECONDS).until(() -> { + boolean result = true; + if (expectMsgReceive.size() != msgReceive.size()) { + return false; + } + if (counter.get() != expectCount) { + return false; + } + for (Integer expectMsg : expectMsgReceive) { + result &= msgReceive.contains(expectMsg); + } + return result; + }); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/offset/OffsetResetIT.java b/test/src/test/java/org/apache/rocketmq/test/offset/OffsetResetIT.java new file mode 100644 index 0000000..150e631 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/offset/OffsetResetIT.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.test.offset; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.admin.ConsumeStats; +import org.apache.rocketmq.remoting.protocol.admin.OffsetWrapper; +import org.apache.rocketmq.remoting.protocol.header.ResetOffsetRequestHeader; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.message.MessageQueueMsg; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; + +import static org.awaitility.Awaitility.await; + +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class OffsetResetIT extends BaseConf { + + private static final Logger LOGGER = LoggerFactory.getLogger(OffsetResetIT.class); + + private RMQNormalListener listener = null; + private RMQNormalProducer producer = null; + private RMQNormalConsumer consumer = null; + private DefaultMQAdminExt defaultMQAdminExt = null; + private String topic = null; + + @Before + public void init() throws MQClientException { + topic = initTopic(); + LOGGER.info(String.format("use topic: %s;", topic)); + + for (BrokerController controller : brokerControllerList) { + controller.getBrokerConfig().setLongPollingEnable(false); + controller.getBrokerConfig().setShortPollingTimeMills(500); + controller.getBrokerConfig().setUseServerSideResetOffset(true); + } + + listener = new RMQNormalListener(); + producer = getProducer(NAMESRV_ADDR, topic); + consumer = getConsumer(NAMESRV_ADDR, topic, "*", listener); + + defaultMQAdminExt = BaseConf.getAdmin(NAMESRV_ADDR); + defaultMQAdminExt.start(); + } + + @After + public void tearDown() { + shutdown(); + } + + @Test + public void testEncodeOffsetHeader() { + ResetOffsetRequestHeader requestHeader = new ResetOffsetRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setGroup(consumer.getConsumerGroup()); + requestHeader.setTimestamp(System.currentTimeMillis()); + requestHeader.setForce(false); + RemotingCommand.createRequestCommand(RequestCode.INVOKE_BROKER_TO_RESET_OFFSET, requestHeader); + } + + /** + * use mq admin tool to query remote offset + */ + private long getConsumerLag(String topic, String group) throws Exception { + long consumerLag = 0L; + for (BrokerController controller : brokerControllerList) { + ConsumeStats consumeStats = defaultMQAdminExt.getDefaultMQAdminExtImpl() + .getMqClientInstance().getMQClientAPIImpl() + .getConsumeStats(controller.getBrokerAddr(), group, topic, 3000); + Map offsetTable = consumeStats.getOffsetTable(); + + for (Map.Entry entry : offsetTable.entrySet()) { + MessageQueue messageQueue = entry.getKey(); + OffsetWrapper offsetWrapper = entry.getValue(); + + Assert.assertEquals(messageQueue.getBrokerName(), controller.getBrokerConfig().getBrokerName()); + long brokerOffset = controller.getMessageStore().getMaxOffsetInQueue(topic, messageQueue.getQueueId()); + long consumerOffset = controller.getConsumerOffsetManager().queryOffset( + consumer.getConsumerGroup(), topic, messageQueue.getQueueId()); + Assert.assertEquals(brokerOffset, offsetWrapper.getBrokerOffset()); + Assert.assertEquals(consumerOffset, offsetWrapper.getConsumerOffset()); + + consumerLag += brokerOffset - consumerOffset; + } + } + return consumerLag; + } + + @Test + public void testResetOffsetSingleQueue() throws Exception { + int msgSize = 100; + List mqs = producer.getMessageQueue(); + MessageQueueMsg messageQueueMsg = new MessageQueueMsg(mqs, msgSize); + + producer.send(messageQueueMsg.getMsgsWithMQ()); + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + await().pollInterval(Duration.ofSeconds(1)).atMost(Duration.ofMinutes(3)).until( + () -> 0L == this.getConsumerLag(topic, consumer.getConsumerGroup())); + + for (BrokerController controller : brokerControllerList) { + defaultMQAdminExt.resetOffsetByQueueId(controller.getBrokerAddr(), + consumer.getConsumerGroup(), consumer.getTopic(), 3, 0); + } + + int hasConsumeBefore = listener.getMsgIndex().get(); + int expectAfterReset = brokerControllerList.size() * msgSize; + await().pollInterval(Duration.ofSeconds(1)).atMost(Duration.ofMinutes(3)).until(() -> { + long receive = listener.getMsgIndex().get(); + long expect = hasConsumeBefore + expectAfterReset; + return receive >= expect; + }); + } + + @Test + public void testResetOffsetTotal() throws Exception { + int msgSize = 100; + long start = System.currentTimeMillis(); + List mqs = producer.getMessageQueue(); + MessageQueueMsg messageQueueMsg = new MessageQueueMsg(mqs, msgSize); + + producer.send(messageQueueMsg.getMsgsWithMQ()); + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + await().pollInterval(Duration.ofSeconds(1)).atMost(Duration.ofMinutes(3)).until( + () -> 0L == this.getConsumerLag(topic, consumer.getConsumerGroup())); + + for (BrokerController controller : brokerControllerList) { + defaultMQAdminExt.getDefaultMQAdminExtImpl().getMqClientInstance().getMQClientAPIImpl() + .invokeBrokerToResetOffset(controller.getBrokerAddr(), + consumer.getTopic(), consumer.getConsumerGroup(), start, true, 3 * 1000); + } + + int hasConsumeBefore = listener.getMsgIndex().get(); + int expectAfterReset = mqs.size() * msgSize; + await().pollInterval(Duration.ofSeconds(1)).atMost(Duration.ofMinutes(3)).until(() -> { + long receive = listener.getMsgIndex().get(); + long expect = hasConsumeBefore + expectAfterReset; + return receive >= expect; + }); + } + + @Test + public void testPullOffsetTotal() throws Exception { + int msgSize = 100; + List mqs = producer.getMessageQueue(); + MessageQueueMsg messageQueueMsg = new MessageQueueMsg(mqs, msgSize); + + producer.send(messageQueueMsg.getMsgsWithMQ()); + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + + await().pollInterval(Duration.ofSeconds(1)).atMost(Duration.ofMinutes(3)).until( + () -> 0L == this.getConsumerLag(topic, consumer.getConsumerGroup())); + + long expectInflight = 0L; + for (BrokerController controller : brokerControllerList) { + ConsumeStats consumeStats = defaultMQAdminExt.getDefaultMQAdminExtImpl().getMqClientInstance() + .getMQClientAPIImpl().getConsumeStats(controller.getBrokerAddr(), + consumer.getConsumerGroup(), consumer.getTopic(), 3 * 1000); + expectInflight += consumeStats.computeInflightTotalDiff(); + } + Assert.assertEquals(0L, expectInflight); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/recall/RecallWithTraceIT.java b/test/src/test/java/org/apache/rocketmq/test/recall/RecallWithTraceIT.java new file mode 100644 index 0000000..d52c700 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/recall/RecallWithTraceIT.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.test.recall; + +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.producer.DefaultMQProducer; +import org.apache.rocketmq.client.trace.TraceContext; +import org.apache.rocketmq.client.trace.TraceDataEncoder; +import org.apache.rocketmq.client.trace.TraceType; +import org.apache.rocketmq.common.attribute.CQType; +import org.apache.rocketmq.common.attribute.TopicMessageType; +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.producer.RecallMessageHandle; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.base.IntegrationTestBase; +import org.apache.rocketmq.test.client.rmq.RMQPopConsumer; +import org.apache.rocketmq.test.factory.ConsumerFactory; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.MQRandomUtils; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.awaitility.Awaitility.await; + +public class RecallWithTraceIT extends BaseConf { + private static String topic; + private static String group; + private static DefaultMQProducer producer; + private static RMQPopConsumer popConsumer; + + @BeforeClass + public static void init() throws MQClientException { + System.setProperty("com.rocketmq.recall.default.trace.enable", Boolean.TRUE.toString()); + topic = MQRandomUtils.getRandomTopic(); + IntegrationTestBase.initTopic(topic, NAMESRV_ADDR, BROKER1_NAME, 1, CQType.SimpleCQ, TopicMessageType.NORMAL); + group = initConsumerGroup(); + producer = new DefaultMQProducer(group, true, topic); + producer.setNamesrvAddr(NAMESRV_ADDR); + producer.start(); + popConsumer = ConsumerFactory.getRMQPopConsumer(NAMESRV_ADDR, group, topic, "*", new RMQNormalListener()); + mqClients.add(popConsumer); + mqClients.add(producer); + } + + @AfterClass + public static void tearDown() { + shutdown(); + } + + @Test + public void testRecallTrace() throws MQBrokerException, RemotingException, InterruptedException, MQClientException { + String msgId = MessageClientIDSetter.createUniqID(); + String recallHandle = RecallMessageHandle.HandleV1.buildHandle(topic, BROKER1_NAME, + String.valueOf(System.currentTimeMillis() + 30000), msgId); + producer.recallMessage(topic, recallHandle); + + MessageQueue messageQueue = new MessageQueue(topic, BROKER1_NAME, 0); + String brokerAddress = brokerController1.getBrokerAddr(); + AtomicReference traceMessage = new AtomicReference(); + await() + .pollInterval(1, TimeUnit.SECONDS) + .atMost(15, TimeUnit.SECONDS) + .until(() -> { + PopResult popResult = popConsumer.pop(brokerAddress, messageQueue, 60 * 1000, -1); + boolean found = popResult.getPopStatus().equals(PopStatus.FOUND); + traceMessage.set(found ? popResult.getMsgFoundList().get(0) : null); + return found; + }); + + Assert.assertNotNull(traceMessage.get()); + TraceContext context = + TraceDataEncoder.decoderFromTraceDataString(new String(traceMessage.get().getBody())).get(0); + Assert.assertEquals(TraceType.Recall, context.getTraceType()); + Assert.assertEquals(group, context.getGroupName()); + Assert.assertTrue(context.isSuccess()); + Assert.assertEquals(msgId, context.getTraceBeans().get(0).getMsgId()); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/recall/SendAndRecallDelayMessageIT.java b/test/src/test/java/org/apache/rocketmq/test/recall/SendAndRecallDelayMessageIT.java new file mode 100644 index 0000000..2fb9e02 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/recall/SendAndRecallDelayMessageIT.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.test.recall; + +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.common.attribute.CQType; +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.producer.RecallMessageHandle; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.base.IntegrationTestBase; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.client.rmq.RMQPopConsumer; +import org.apache.rocketmq.test.factory.ConsumerFactory; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.MQRandomUtils; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.awaitility.Awaitility.await; + +public class SendAndRecallDelayMessageIT extends BaseConf { + + private static String initTopic; + private static String consumerGroup; + private static RMQNormalProducer producer; + private static RMQPopConsumer popConsumer; + + @BeforeClass + public static void init() { + initTopic = initTopic(); + consumerGroup = initConsumerGroup(); + producer = getProducer(NAMESRV_ADDR, initTopic); + popConsumer = ConsumerFactory.getRMQPopConsumer(NAMESRV_ADDR, consumerGroup, initTopic, "*", new RMQNormalListener()); + mqClients.add(popConsumer); + } + + @AfterClass + public static void tearDown() { + shutdown(); + } + + @Test + public void testSendAndRecv() throws Exception { + int delaySecond = 1; + String topic = MQRandomUtils.getRandomTopic(); + IntegrationTestBase.initTopic(topic, NAMESRV_ADDR, BROKER1_NAME, 1, CQType.SimpleCQ, TopicMessageType.DELAY); + MessageQueue messageQueue = new MessageQueue(topic, BROKER1_NAME, 0); + String brokerAddress = brokerController1.getBrokerAddr(); + + List sendList = buildSendMessageList(topic, delaySecond); + List recvList = new ArrayList<>(); + + for (Message message : sendList) { + producer.getProducer().send(message); + } + + await() + .pollInterval(1, TimeUnit.SECONDS) + .atMost(delaySecond + 15, TimeUnit.SECONDS) + .until(() -> { + PopResult popResult = popConsumer.pop(brokerAddress, messageQueue, 60 * 1000, -1); + processPopResult(recvList, popResult); + return recvList.size() == sendList.size(); + }); + } + + @Test + public void testSendAndRecall() throws Exception { + int delaySecond = 5; + String topic = MQRandomUtils.getRandomTopic(); + IntegrationTestBase.initTopic(topic, NAMESRV_ADDR, BROKER1_NAME, 1, CQType.SimpleCQ, TopicMessageType.DELAY); + MessageQueue messageQueue = new MessageQueue(topic, BROKER1_NAME, 0); + String brokerAddress = brokerController1.getBrokerAddr(); + + List sendList = buildSendMessageList(topic, delaySecond); + List recvList = new ArrayList<>(); + int recallCount = 0; + + for (Message message : sendList) { + SendResult sendResult = producer.getProducer().send(message); + if (sendResult.getRecallHandle() != null) { + String messageId = producer.getProducer().recallMessage(topic, sendResult.getRecallHandle()); + assertEquals(sendResult.getMsgId(), messageId); + recallCount += 1; + } + } + assertEquals(sendList.size() - 2, recallCount); // one normal and one delay-level message + try { + await() + .pollInterval(1, TimeUnit.SECONDS) + .atMost(delaySecond + 15, TimeUnit.SECONDS) + .until(() -> { + PopResult popResult = popConsumer.pop(brokerAddress, messageQueue, 60 * 1000, -1); + processPopResult(recvList, popResult); + return recvList.size() == sendList.size(); + }); + } catch (Exception e) { + } + assertEquals(sendList.size() - recallCount, recvList.size()); + } + + @Test + public void testSendAndRecall_ukCollision() throws Exception { + int delaySecond = 5; + String topic = MQRandomUtils.getRandomTopic(); + String collisionTopic = MQRandomUtils.getRandomTopic(); + IntegrationTestBase.initTopic(topic, NAMESRV_ADDR, BROKER1_NAME, 1, CQType.SimpleCQ, TopicMessageType.DELAY); + IntegrationTestBase.initTopic(collisionTopic, NAMESRV_ADDR, BROKER1_NAME, 1, CQType.SimpleCQ, TopicMessageType.DELAY); + MessageQueue messageQueue = new MessageQueue(topic, BROKER1_NAME, 0); + String brokerAddress = brokerController1.getBrokerAddr(); + + List sendList = buildSendMessageList(topic, delaySecond); + List recvList = new ArrayList<>(); + int recallCount = 0; + + for (Message message : sendList) { + SendResult sendResult = producer.getProducer().send(message); + if (sendResult.getRecallHandle() != null) { + RecallMessageHandle.HandleV1 handleEntity = + (RecallMessageHandle.HandleV1) RecallMessageHandle.decodeHandle(sendResult.getRecallHandle()); + String collisionHandle = RecallMessageHandle.HandleV1.buildHandle(collisionTopic, + handleEntity.getBrokerName(), handleEntity.getTimestampStr(), handleEntity.getMessageId()); + String messageId = producer.getProducer().recallMessage(collisionTopic, collisionHandle); + assertEquals(sendResult.getMsgId(), messageId); + recallCount += 1; + } + } + assertEquals(sendList.size() - 2, recallCount); // one normal and one delay-level message + + try { + await() + .pollInterval(1, TimeUnit.SECONDS) + .atMost(delaySecond + 15, TimeUnit.SECONDS) + .until(() -> { + PopResult popResult = popConsumer.pop(brokerAddress, messageQueue, 60 * 1000, -1); + processPopResult(recvList, popResult); + return recvList.size() == sendList.size(); + }); + } catch (Exception e) { + } + assertEquals(sendList.size(), recvList.size()); + } + + private void processPopResult(List recvList, PopResult popResult) { + if (popResult.getPopStatus() == PopStatus.FOUND && popResult.getMsgFoundList() != null) { + recvList.addAll(popResult.getMsgFoundList()); + } + } + + private List buildSendMessageList(String topic, int delaySecond) { + Message msg0 = new Message(topic, "tag", "Hello RocketMQ".getBytes()); // not supported + + Message msg1 = new Message(topic, "tag", "Hello RocketMQ".getBytes()); // not supported + msg1.setDelayTimeLevel(2); + + Message msg2 = new Message(topic, "tag", "Hello RocketMQ".getBytes()); + msg2.setDelayTimeMs(delaySecond * 1000L); + + Message msg3 = new Message(topic, "tag", "Hello RocketMQ".getBytes()); + msg3.setDelayTimeSec(delaySecond); + + Message msg4 = new Message(topic, "tag", "Hello RocketMQ".getBytes()); + msg4.setDeliverTimeMs(System.currentTimeMillis() + delaySecond * 1000L); + + return Arrays.asList(msg0, msg1, msg2, msg3, msg4); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/retry/PopConsumerRetryIT.java b/test/src/test/java/org/apache/rocketmq/test/retry/PopConsumerRetryIT.java new file mode 100644 index 0000000..39b5ddc --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/retry/PopConsumerRetryIT.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.test.retry; + +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.rocketmq.client.AccessChannel; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; +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.TopicConfig; +import org.apache.rocketmq.common.attribute.CQType; +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.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.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.base.IntegrationTestBase; +import org.apache.rocketmq.test.offset.OffsetResetIT; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static org.awaitility.Awaitility.await; + +public class PopConsumerRetryIT extends BaseConf { + + private static final Logger LOGGER = LoggerFactory.getLogger(OffsetResetIT.class); + + private DefaultMQAdminExt defaultMQAdminExt = null; + private String topicName = null; + private String groupName = null; + + @Before + public void init() throws MQClientException { + topicName = "topic-" + RandomStringUtils.randomAlphabetic(72).toUpperCase(); + groupName = "group-" + RandomStringUtils.randomAlphabetic(72).toUpperCase(); + LOGGER.info(String.format("use topic: %s, group: %s", topicName, groupName)); + IntegrationTestBase.initTopic(topicName, NAMESRV_ADDR, CLUSTER_NAME, CQType.SimpleCQ); + defaultMQAdminExt = getAdmin(NAMESRV_ADDR); + defaultMQAdminExt.start(); + } + + @After + public void tearDown() { + shutdown(); + } + + private void switchPop(String groupName, String topicName) throws Exception { + ClusterInfo clusterInfo = defaultMQAdminExt.examineBrokerClusterInfo(); + Set brokerAddrs = clusterInfo.getBrokerAddrTable().values() + .stream().map(BrokerData::selectBrokerAddr).collect(Collectors.toSet()); + for (String brokerAddr : brokerAddrs) { + TopicConfig topicConfig = new TopicConfig(topicName, 1, 1, 6); + defaultMQAdminExt.createAndUpdateTopicConfig(brokerAddr, topicConfig); + defaultMQAdminExt.setMessageRequestMode(brokerAddr, topicName, groupName, + MessageRequestMode.POP, 8, 3000L); + } + } + + @Test + public void testNormalMessageUseMessageVersionV2() throws Exception { + switchPop(groupName, topicName); + + AtomicInteger successCount = new AtomicInteger(); + AtomicInteger retryCount = new AtomicInteger(); + + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(groupName); + consumer.subscribe(topicName, "*"); + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); + consumer.setConsumeThreadMin(1); + consumer.setConsumeThreadMax(1); + consumer.setConsumeMessageBatchMaxSize(1); + consumer.setNamesrvAddr(NAMESRV_ADDR); + consumer.setClientRebalance(false); + consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + for (MessageExt message : msgs) { + LOGGER.debug(String.format("messageId: %s, times: %d, topic: %s", + message.getMsgId(), message.getReconsumeTimes(), message.getTopic())); + if (message.getReconsumeTimes() < 2) { + retryCount.incrementAndGet(); + return ConsumeConcurrentlyStatus.RECONSUME_LATER; + } else { + successCount.incrementAndGet(); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + consumer.start(); + LOGGER.info("Consumer Started..."); + + DefaultMQProducer producer = new DefaultMQProducer("PID-1", false, null); + producer.setAccessChannel(AccessChannel.CLOUD); + producer.setNamesrvAddr(NAMESRV_ADDR); + producer.start(); + LOGGER.info("Producer Started...%n"); + + // wait pop client register + TimeUnit.SECONDS.sleep(3); + + int total = 10; + for (int i = 0; i < total; i++) { + Message msg = new Message( + topicName, "*", "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + SendResult sendResult = producer.send(msg); + Assert.assertEquals(SendStatus.SEND_OK, sendResult.getSendStatus()); + } + + await().pollInterval(1, TimeUnit.SECONDS).atMost(90, TimeUnit.SECONDS) + .until(() -> { + LOGGER.debug(String.format("retry: %d, success: %d", retryCount.get(), successCount.get())); + return retryCount.get() == total * 2 && successCount.get() == total; + }); + } + + @Test + public void testFIFOMessageUseMessageVersionV2() throws Exception { + switchPop(groupName, topicName); + + AtomicInteger successCount = new AtomicInteger(); + AtomicInteger retryCount = new AtomicInteger(); + + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(groupName); + consumer.subscribe(topicName, "*"); + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); + consumer.setConsumeThreadMin(1); + consumer.setConsumeThreadMax(1); + consumer.setConsumeMessageBatchMaxSize(1); + consumer.setNamesrvAddr(NAMESRV_ADDR); + consumer.setClientRebalance(false); + consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> { + for (MessageExt message : msgs) { + LOGGER.debug(String.format("messageId: %s, times: %d, topic: %s", + message.getMsgId(), message.getReconsumeTimes(), message.getTopic())); + if (message.getReconsumeTimes() < 2) { + retryCount.incrementAndGet(); + return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT; + } else { + successCount.incrementAndGet(); + return ConsumeOrderlyStatus.SUCCESS; + } + } + return ConsumeOrderlyStatus.SUCCESS; + }); + consumer.start(); + LOGGER.info("Consumer Started..."); + + DefaultMQProducer producer = new DefaultMQProducer("PID-1", false, null); + producer.setAccessChannel(AccessChannel.CLOUD); + producer.setNamesrvAddr(NAMESRV_ADDR); + producer.start(); + LOGGER.info("Producer Started...%n"); + + // wait pop client register + TimeUnit.SECONDS.sleep(3); + + int total = 10; + for (int i = 0; i < total; i++) { + Message msg = new Message( + topicName, "*", "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + SendResult sendResult = producer.send(msg); + Assert.assertEquals(SendStatus.SEND_OK, sendResult.getSendStatus()); + } + + await().pollInterval(1, TimeUnit.SECONDS).atMost(90, TimeUnit.SECONDS) + .until(() -> { + LOGGER.debug(String.format("retry: %d, success: %d", retryCount.get(), successCount.get())); + return retryCount.get() == total * 2 && successCount.get() == total; + }); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/route/CreateAndUpdateTopicIT.java b/test/src/test/java/org/apache/rocketmq/test/route/CreateAndUpdateTopicIT.java new file mode 100644 index 0000000..9e9afb1 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/route/CreateAndUpdateTopicIT.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.test.route; + +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.util.MQAdminTestUtils; +import org.junit.Ignore; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public class CreateAndUpdateTopicIT extends BaseConf { + + @Test + public void testCreateOrUpdateTopic_EnableSingleTopicRegistration() { + String topic = "test-topic-without-broker-registration"; + brokerController1.getBrokerConfig().setEnableSingleTopicRegister(true); + brokerController2.getBrokerConfig().setEnableSingleTopicRegister(true); + brokerController3.getBrokerConfig().setEnableSingleTopicRegister(true); + + final boolean createResult = MQAdminTestUtils.createTopic(NAMESRV_ADDR, CLUSTER_NAME, topic, 8, null); + assertThat(createResult).isTrue(); + + TopicRouteData route = MQAdminTestUtils.examineTopicRouteInfo(NAMESRV_ADDR, topic); + assertThat(route.getBrokerDatas()).hasSize(3); + assertThat(route.getQueueDatas()).hasSize(3); + + brokerController1.getBrokerConfig().setEnableSingleTopicRegister(false); + brokerController2.getBrokerConfig().setEnableSingleTopicRegister(false); + brokerController3.getBrokerConfig().setEnableSingleTopicRegister(false); + + } + + // Temporarily ignore the fact that this test cannot pass in the integration test pipeline due to unknown reasons + @Ignore + @Test + public void testDeleteTopicFromNameSrvWithBrokerRegistration() { + namesrvController.getNamesrvConfig().setDeleteTopicWithBrokerRegistration(true); + brokerController1.getBrokerConfig().setEnableSingleTopicRegister(true); + brokerController2.getBrokerConfig().setEnableSingleTopicRegister(true); + brokerController3.getBrokerConfig().setEnableSingleTopicRegister(true); + + String testTopic1 = "test-topic-keep-route"; + String testTopic2 = "test-topic-delete-route"; + + boolean createResult = MQAdminTestUtils.createTopic(NAMESRV_ADDR, CLUSTER_NAME, testTopic1, 8, null); + assertThat(createResult).isTrue(); + + createResult = MQAdminTestUtils.createTopic(NAMESRV_ADDR, CLUSTER_NAME, testTopic2, 8, null); + assertThat(createResult).isTrue(); + + TopicRouteData route = MQAdminTestUtils.examineTopicRouteInfo(NAMESRV_ADDR, testTopic2); + assertThat(route.getBrokerDatas()).hasSize(3); + + MQAdminTestUtils.deleteTopicFromBrokerOnly(NAMESRV_ADDR, BROKER1_NAME, testTopic2); + + // Deletion is lazy, trigger broker registration + brokerController1.registerBrokerAll(false, false, true); + + await().atMost(10, TimeUnit.SECONDS).until(() -> { + // The route info of testTopic2 will be removed from broker1 after the registration + TopicRouteData finalRoute = MQAdminTestUtils.examineTopicRouteInfo(NAMESRV_ADDR, testTopic2); + return finalRoute.getBrokerDatas().size() == 2 + && finalRoute.getQueueDatas().get(0).getBrokerName().equals(BROKER2_NAME) + && finalRoute.getQueueDatas().get(1).getBrokerName().equals(BROKER3_NAME); + }); + + brokerController1.getBrokerConfig().setEnableSingleTopicRegister(false); + brokerController2.getBrokerConfig().setEnableSingleTopicRegister(false); + brokerController3.getBrokerConfig().setEnableSingleTopicRegister(false); + namesrvController.getNamesrvConfig().setDeleteTopicWithBrokerRegistration(false); + } + + @Test + public void testStaticTopicNotAffected() throws Exception { + namesrvController.getNamesrvConfig().setDeleteTopicWithBrokerRegistration(true); + brokerController1.getBrokerConfig().setEnableSingleTopicRegister(true); + brokerController2.getBrokerConfig().setEnableSingleTopicRegister(true); + brokerController3.getBrokerConfig().setEnableSingleTopicRegister(true); + + String testTopic = "test-topic-not-affected"; + String testStaticTopic = "test-static-topic"; + + boolean createResult = MQAdminTestUtils.createTopic(NAMESRV_ADDR, CLUSTER_NAME, testTopic, 8, null); + assertThat(createResult).isTrue(); + + TopicRouteData route = MQAdminTestUtils.examineTopicRouteInfo(NAMESRV_ADDR, testTopic); + assertThat(route.getBrokerDatas()).hasSize(3); + assertThat(route.getQueueDatas()).hasSize(3); + + MQAdminTestUtils.createStaticTopicWithCommand(testStaticTopic, 10, null, CLUSTER_NAME, NAMESRV_ADDR); + + assertThat(route.getBrokerDatas()).hasSize(3); + assertThat(route.getQueueDatas()).hasSize(3); + + brokerController1.getBrokerConfig().setEnableSingleTopicRegister(false); + brokerController2.getBrokerConfig().setEnableSingleTopicRegister(false); + brokerController3.getBrokerConfig().setEnableSingleTopicRegister(false); + namesrvController.getNamesrvConfig().setDeleteTopicWithBrokerRegistration(false); + } + + @Test + public void testCreateOrUpdateTopic_EnableSplitRegistration() { + brokerController1.getBrokerConfig().setEnableSplitRegistration(true); + brokerController2.getBrokerConfig().setEnableSplitRegistration(true); + brokerController3.getBrokerConfig().setEnableSplitRegistration(true); + + String testTopic = "test-topic-"; + + for (int i = 0; i < 10; i++) { + TopicConfig topicConfig = new TopicConfig(testTopic + i, 8, 8); + brokerController1.getTopicConfigManager().updateTopicConfig(topicConfig); + brokerController2.getTopicConfigManager().updateTopicConfig(topicConfig); + brokerController3.getTopicConfigManager().updateTopicConfig(topicConfig); + } + + brokerController1.registerBrokerAll(false, true, true); + brokerController2.registerBrokerAll(false, true, true); + brokerController3.registerBrokerAll(false, true, true); + + for (int i = 0; i < 10; i++) { + TopicRouteData route = MQAdminTestUtils.examineTopicRouteInfo(NAMESRV_ADDR, testTopic + i); + assertThat(route.getBrokerDatas()).hasSize(3); + assertThat(route.getQueueDatas()).hasSize(3); + } + + brokerController1.getBrokerConfig().setEnableSplitRegistration(false); + brokerController2.getBrokerConfig().setEnableSplitRegistration(false); + brokerController3.getBrokerConfig().setEnableSplitRegistration(false); + } +} diff --git a/test/src/test/java/org/apache/rocketmq/test/schema/SchemaTest.java b/test/src/test/java/org/apache/rocketmq/test/schema/SchemaTest.java new file mode 100644 index 0000000..6625081 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/schema/SchemaTest.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.test.schema; + +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + + + +public class SchemaTest { + private static final String BASE_SCHEMA_PATH = "src/test/resources/schema"; + private static final String ADD = "ADD"; + private static final String DELETE = "DELETE"; + private static final String CHANGE = "CHANGE"; + + + + public void generate() throws Exception { + SchemaDefiner.doLoad(); + SchemaTools.write(SchemaTools.generate(SchemaDefiner.API_CLASS_LIST), BASE_SCHEMA_PATH, "api"); + SchemaTools.write(SchemaTools.generate(SchemaDefiner.PROTOCOL_CLASS_LIST), BASE_SCHEMA_PATH, "protocol"); + } + + @Test + @Ignore + public void checkSchema() throws Exception { + SchemaDefiner.doLoad(); + Map> schemaFromFile = new HashMap<>(); + { + schemaFromFile.putAll(SchemaTools.load(BASE_SCHEMA_PATH, SchemaTools.PATH_API)); + schemaFromFile.putAll(SchemaTools.load(BASE_SCHEMA_PATH, SchemaTools.PATH_PROTOCOL)); + } + Map> schemaFromCode = new HashMap<>(); + { + schemaFromCode.putAll(SchemaTools.generate(SchemaDefiner.API_CLASS_LIST)); + schemaFromCode.putAll(SchemaTools.generate(SchemaDefiner.PROTOCOL_CLASS_LIST)); + } + + Map fileChanges = new TreeMap<>(); + schemaFromFile.keySet().forEach(x -> { + if (!schemaFromCode.containsKey(x)) { + fileChanges.put(x, DELETE); + } + }); + schemaFromCode.keySet().forEach(x -> { + if (!schemaFromFile.containsKey(x)) { + fileChanges.put(x, ADD); + } + }); + + Map> changesByFile = new HashMap<>(); + schemaFromFile.forEach((file, oldSchema) -> { + Map newSchema = schemaFromCode.get(file); + Map schemaChanges = new TreeMap<>(); + oldSchema.forEach((k, v) -> { + if (!newSchema.containsKey(k)) { + schemaChanges.put(k, DELETE); + } else if (!newSchema.get(k).equals(v)) { + schemaChanges.put(k, CHANGE); + } + }); + + newSchema.forEach((k, v) -> { + if (!oldSchema.containsKey(k)) { + schemaChanges.put(k, ADD); + } + }); + if (!schemaChanges.isEmpty()) { + changesByFile.put(file, schemaChanges); + } + }); + + fileChanges.forEach((k,v) -> { + System.out.printf("%s file %s\n", v, k); + }); + + changesByFile.forEach((k, v) -> { + System.out.printf("%s file %s:\n", CHANGE, k); + v.forEach((kk, vv) -> { + System.out.printf("\t%s %s\n", vv, kk); + }); + }); + + String message = "The schema test failed, which means you have changed the API or Protocol defined in org.apache.rocketmq.test.schema.SchemaDefiner.\n" + + "Please submit a pr only contains the API/Protocol changes and request at least one PMC Member's review.\n" + + "For original motivation of this test, please refer to https://github.com/apache/rocketmq/pull/4565 ."; + Assert.assertTrue(message, fileChanges.isEmpty() && changesByFile.isEmpty()); + } + +} diff --git a/test/src/test/java/org/apache/rocketmq/test/smoke/NormalMessageSendAndRecvIT.java b/test/src/test/java/org/apache/rocketmq/test/smoke/NormalMessageSendAndRecvIT.java new file mode 100644 index 0000000..1876cee --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/smoke/NormalMessageSendAndRecvIT.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.test.smoke; + +import com.google.common.collect.ImmutableList; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.message.MessageClientExt; +import org.apache.rocketmq.common.message.MessageConst; +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.admin.ConsumeStats; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.base.IntegrationTestBase; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.awaitility.Awaitility; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static com.google.common.truth.Truth.assertThat; + +public class NormalMessageSendAndRecvIT extends BaseConf { + private static Logger logger = LoggerFactory.getLogger(NormalMessageSendAndRecvIT.class); + private RMQNormalConsumer consumer = null; + private RMQNormalProducer producer = null; + private String topic = null; + private String group = null; + private DefaultMQAdminExt defaultMQAdminExt; + + @Before + public void setUp() throws Exception { + topic = initTopic(); + group = initConsumerGroup(); + logger.info(String.format("use topic: %s;", topic)); + producer = getProducer(NAMESRV_ADDR, topic); + consumer = getConsumer(NAMESRV_ADDR, group, topic, "*", new RMQNormalListener()); + defaultMQAdminExt = getAdmin(NAMESRV_ADDR); + defaultMQAdminExt.start(); + } + + @After + public void tearDown() { + BaseConf.shutdown(); + } + + @Test + public void testSynSendMessage() throws Exception { + AtomicReference> messageQueueList = new AtomicReference<>(); + AtomicReference consumeStats = new AtomicReference<>(); + Awaitility.await().atMost(Duration.ofSeconds(120)) + .until(() -> { + try { + consumeStats.set(defaultMQAdminExt.examineConsumeStats(group)); + messageQueueList.set(producer.getProducer().fetchPublishMessageQueues(topic)); + return !messageQueueList.get().isEmpty() && null != consumeStats.get() + && consumeStats.get().getOffsetTable().keySet().containsAll(messageQueueList.get()); + } catch (MQClientException e) { + logger.debug("Exception raised while checking producer and consumer are started", e); + } + return false; + }); + + int msgSize = 10; + for (MessageQueue messageQueue : messageQueueList.get()) { + producer.send(msgSize, messageQueue); + } + Assert.assertEquals("Not all sent succeeded", msgSize * messageQueueList.get().size(), + producer.getAllUndupMsgBody().size()); + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), CONSUME_TIME); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + + for (Object o : consumer.getListener().getAllOriginMsg()) { + MessageClientExt msg = (MessageClientExt) o; + assertThat(msg.getProperty(MessageConst.PROPERTY_POP_CK)).isNull(); + } + //shutdown to persist the offset + consumer.getConsumer().shutdown(); + consumeStats.set(defaultMQAdminExt.examineConsumeStats(group)); + //+1 for the retry topic + for (MessageQueue messageQueue : messageQueueList.get()) { + Assert.assertTrue(consumeStats.get().getOffsetTable().containsKey(messageQueue)); + Assert.assertEquals(msgSize, consumeStats.get().getOffsetTable().get(messageQueue).getConsumerOffset()); + Assert.assertEquals(msgSize, consumeStats.get().getOffsetTable().get(messageQueue).getBrokerOffset()); + } + + } + + @Test + public void testSynSendMessageWhenEnableBuildConsumeQueueConcurrently() throws Exception { + resetStoreConfigWithEnableBuildConsumeQueueConcurrently(true); + + testSynSendMessage(); + + resetStoreConfigWithEnableBuildConsumeQueueConcurrently(false); + } + + void resetStoreConfigWithEnableBuildConsumeQueueConcurrently(boolean enableBuildConsumeQueueConcurrently) { + { + brokerController1.shutdown(); + MessageStoreConfig storeConfig = brokerController1.getMessageStoreConfig(); + BrokerConfig brokerConfig = brokerController1.getBrokerConfig(); + storeConfig.setEnableBuildConsumeQueueConcurrently(enableBuildConsumeQueueConcurrently); + brokerController1 = IntegrationTestBase.createAndStartBroker(storeConfig, brokerConfig); + } + { + brokerController2.shutdown(); + MessageStoreConfig storeConfig = brokerController2.getMessageStoreConfig(); + BrokerConfig brokerConfig = brokerController2.getBrokerConfig(); + storeConfig.setEnableBuildConsumeQueueConcurrently(enableBuildConsumeQueueConcurrently); + brokerController2 = IntegrationTestBase.createAndStartBroker(storeConfig, brokerConfig); + } + { + brokerController3.shutdown(); + MessageStoreConfig storeConfig = brokerController3.getMessageStoreConfig(); + BrokerConfig brokerConfig = brokerController3.getBrokerConfig(); + storeConfig.setEnableBuildConsumeQueueConcurrently(enableBuildConsumeQueueConcurrently); + brokerController3 = IntegrationTestBase.createAndStartBroker(storeConfig, brokerConfig); + } + brokerControllerList = ImmutableList.of(brokerController1, brokerController2, brokerController3); + brokerControllerMap = brokerControllerList.stream().collect( + Collectors.toMap(input -> input.getBrokerConfig().getBrokerName(), Function.identity())); + } + +} diff --git a/test/src/test/java/org/apache/rocketmq/test/statictopic/StaticTopicIT.java b/test/src/test/java/org/apache/rocketmq/test/statictopic/StaticTopicIT.java new file mode 100644 index 0000000..8cbcdda --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/statictopic/StaticTopicIT.java @@ -0,0 +1,512 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.test.statictopic; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import java.util.ArrayList; +import java.util.Collection; +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.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.common.MixAll; +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.OffsetWrapper; +import org.apache.rocketmq.remoting.protocol.admin.TopicStatsTable; +import org.apache.rocketmq.remoting.protocol.statictopic.LogicQueueMappingItem; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicConfigAndQueueMapping; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingOne; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingUtils; +import org.apache.rocketmq.remoting.rpc.ClientMetadata; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.MQAdminTestUtils; +import org.apache.rocketmq.test.util.MQRandomUtils; +import org.apache.rocketmq.test.util.TestUtils; +import org.apache.rocketmq.test.util.VerifyUtils; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.admin.MQAdminUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.FixMethodOrder; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; +import static org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingUtils.getMappingDetailFromConfig; + +@FixMethodOrder +public class StaticTopicIT extends BaseConf { + + private static Logger logger = LoggerFactory.getLogger(StaticTopicIT.class); + private DefaultMQAdminExt defaultMQAdminExt; + + @Before + public void setUp() throws Exception { + System.setProperty("rocketmq.client.rebalance.waitInterval", "500"); + defaultMQAdminExt = getAdmin(NAMESRV_ADDR); + waitBrokerRegistered(NAMESRV_ADDR, CLUSTER_NAME, BROKER_NUM); + defaultMQAdminExt.start(); + } + + + @Test + public void testCommandsWithCluster() throws Exception { + //This case is used to mock the env to test the command manually + String topic = "static" + MQRandomUtils.getRandomTopic(); + RMQNormalProducer producer = getProducer(NAMESRV_ADDR, topic); + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + int queueNum = 10; + int msgEachQueue = 100; + + { + MQAdminTestUtils.createStaticTopicWithCommand(topic, queueNum, null, CLUSTER_NAME, NAMESRV_ADDR); + sendMessagesAndCheck(producer, getBrokers(), topic, queueNum, msgEachQueue, 0); + //consume and check + consumeMessagesAndCheck(producer, consumer, topic, queueNum, msgEachQueue, 0, 1); + } + { + MQAdminTestUtils.remappingStaticTopicWithCommand(topic, null, CLUSTER_NAME, NAMESRV_ADDR); + awaitRefreshStaticTopicMetadata(3000, topic, producer.getProducer(), consumer.getConsumer(), defaultMQAdminExt); + sendMessagesAndCheck(producer, getBrokers(), topic, queueNum, msgEachQueue, msgEachQueue); + } + } + + @Test + public void testCommandsWithBrokers() throws Exception { + //This case is used to mock the env to test the command manually + String topic = "static" + MQRandomUtils.getRandomTopic(); + RMQNormalProducer producer = getProducer(NAMESRV_ADDR, topic); + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + int queueNum = 10; + int msgEachQueue = 10; + { + Set brokers = ImmutableSet.of(BROKER1_NAME); + MQAdminTestUtils.createStaticTopicWithCommand(topic, queueNum, brokers, null, NAMESRV_ADDR); + sendMessagesAndCheck(producer, brokers, topic, queueNum, msgEachQueue, 0); + //consume and check + consumeMessagesAndCheck(producer, consumer, topic, queueNum, msgEachQueue, 0, 1); + } + { + Set brokers = ImmutableSet.of(BROKER2_NAME); + MQAdminTestUtils.remappingStaticTopicWithCommand(topic, brokers, null, NAMESRV_ADDR); + awaitRefreshStaticTopicMetadata(3000, topic, producer.getProducer(), consumer.getConsumer(), defaultMQAdminExt); + sendMessagesAndCheck(producer, brokers, topic, queueNum, msgEachQueue, TopicQueueMappingUtils.DEFAULT_BLOCK_SEQ_SIZE); + consumeMessagesAndCheck(producer, consumer, topic, queueNum, msgEachQueue, 0, 2); + } + } + + @Test + public void testNoTargetBrokers() throws Exception { + String topic = "static" + MQRandomUtils.getRandomTopic(); + int queueNum = 10; + { + Set targetBrokers = new HashSet<>(); + targetBrokers.add(BROKER1_NAME); + MQAdminTestUtils.createStaticTopic(topic, queueNum, targetBrokers, defaultMQAdminExt); + Map remoteBrokerConfigMap = MQAdminUtils.examineTopicConfigAll(topic, defaultMQAdminExt); + Assert.assertEquals(BROKER_NUM, remoteBrokerConfigMap.size()); + TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, remoteBrokerConfigMap); + Map globalIdMap = TopicQueueMappingUtils.checkAndBuildMappingItems(new ArrayList<>(getMappingDetailFromConfig(remoteBrokerConfigMap.values())), false, true); + Assert.assertEquals(queueNum, globalIdMap.size()); + TopicConfigAndQueueMapping configMapping = remoteBrokerConfigMap.get(BROKER2_NAME); + Assert.assertEquals(0, configMapping.getWriteQueueNums()); + Assert.assertEquals(0, configMapping.getReadQueueNums()); + Assert.assertEquals(0, configMapping.getMappingDetail().getHostedQueues().size()); + } + + { + Set targetBrokers = new HashSet<>(); + targetBrokers.add(BROKER2_NAME); + MQAdminTestUtils.remappingStaticTopic(topic, targetBrokers, defaultMQAdminExt); + Map remoteBrokerConfigMap = MQAdminUtils.examineTopicConfigAll(topic, defaultMQAdminExt); + Assert.assertEquals(BROKER_NUM, remoteBrokerConfigMap.size()); + TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, remoteBrokerConfigMap); + Map globalIdMap = TopicQueueMappingUtils.checkAndBuildMappingItems(new ArrayList<>(getMappingDetailFromConfig(remoteBrokerConfigMap.values())), false, true); + Assert.assertEquals(queueNum, globalIdMap.size()); + } + + } + + private void sendMessagesAndCheck(RMQNormalProducer producer, Set targetBrokers, String topic, int queueNum, int msgEachQueue, long baseOffset) throws Exception { + ClientMetadata clientMetadata = MQAdminUtils.getBrokerAndTopicMetadata(topic, defaultMQAdminExt); + List messageQueueList = producer.getMessageQueue(); + Assert.assertEquals(queueNum, messageQueueList.size()); + for (int i = 0; i < queueNum; i++) { + MessageQueue messageQueue = messageQueueList.get(i); + Assert.assertEquals(topic, messageQueue.getTopic()); + Assert.assertEquals(TopicQueueMappingUtils.getMockBrokerName(MixAll.METADATA_SCOPE_GLOBAL), messageQueue.getBrokerName()); + Assert.assertEquals(i, messageQueue.getQueueId()); + String destBrokerName = clientMetadata.getBrokerNameFromMessageQueue(messageQueue); + Assert.assertTrue(targetBrokers.contains(destBrokerName)); + } + for (MessageQueue messageQueue: messageQueueList) { + producer.send(msgEachQueue, messageQueue); + } + Assert.assertEquals(0, producer.getSendErrorMsg().size()); + //leave the time to build the cq + Assert.assertTrue(awaitDispatchMs(500)); + for (MessageQueue messageQueue : messageQueueList) { + Assert.assertEquals(0, defaultMQAdminExt.minOffset(messageQueue)); + Assert.assertEquals(msgEachQueue + baseOffset, defaultMQAdminExt.maxOffset(messageQueue)); + } + TopicStatsTable topicStatsTable = defaultMQAdminExt.examineTopicStats(topic); + for (MessageQueue messageQueue : messageQueueList) { + Assert.assertEquals(0, topicStatsTable.getOffsetTable().get(messageQueue).getMinOffset()); + Assert.assertEquals(msgEachQueue + baseOffset, topicStatsTable.getOffsetTable().get(messageQueue).getMaxOffset()); + } + } + + private Map> computeMessageByQueue(Collection msgs) { + Map> messagesByQueue = new HashMap<>(); + for (Object object : msgs) { + MessageExt messageExt = (MessageExt) object; + if (!messagesByQueue.containsKey(messageExt.getQueueId())) { + messagesByQueue.put(messageExt.getQueueId(), new ArrayList<>()); + } + messagesByQueue.get(messageExt.getQueueId()).add(messageExt); + } + for (List msgEachQueue : messagesByQueue.values()) { + msgEachQueue.sort((o1, o2) -> (int) (o1.getQueueOffset() - o2.getQueueOffset())); + } + return messagesByQueue; + } + + private void consumeMessagesAndCheck(RMQNormalProducer producer, RMQNormalConsumer consumer, String topic, int queueNum, int msgEachQueue, int startGen, int genNum) { + consumer.getListener().waitForMessageConsume(producer.getAllMsgBody(), 60000); + + Assert.assertEquals(producer.getAllMsgBody().size(), consumer.getListener().getAllMsgBody().size()); + assertThat(VerifyUtils.getFilterdMessage(producer.getAllMsgBody(), + consumer.getListener().getAllMsgBody())) + .containsExactlyElementsIn(producer.getAllMsgBody()); + Map> messagesByQueue = computeMessageByQueue(consumer.getListener().getAllOriginMsg()); + Assert.assertEquals(queueNum, messagesByQueue.size()); + for (int i = 0; i < queueNum; i++) { + List messageExts = messagesByQueue.get(i); + + int totalEachQueue = msgEachQueue * genNum; + Assert.assertEquals(totalEachQueue, messageExts.size()); + for (int j = 0; j < totalEachQueue; j++) { + MessageExt messageExt = messageExts.get(j); + int currGen = startGen + j / msgEachQueue; + Assert.assertEquals(topic, messageExt.getTopic()); + Assert.assertEquals(TopicQueueMappingUtils.getMockBrokerName(MixAll.METADATA_SCOPE_GLOBAL), messageExt.getBrokerName()); + Assert.assertEquals(i, messageExt.getQueueId()); + Assert.assertEquals((j % msgEachQueue) + currGen * TopicQueueMappingUtils.DEFAULT_BLOCK_SEQ_SIZE, messageExt.getQueueOffset()); + } + } + } + + + @Test + public void testCreateProduceConsumeStaticTopic() throws Exception { + String topic = "static" + MQRandomUtils.getRandomTopic(); + RMQNormalProducer producer = getProducer(NAMESRV_ADDR, topic); + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + + int queueNum = 10; + int msgEachQueue = 10; + //create static topic + Map localBrokerConfigMap = MQAdminTestUtils.createStaticTopic(topic, queueNum, getBrokers(), defaultMQAdminExt); + //check the static topic config + { + Map remoteBrokerConfigMap = MQAdminUtils.examineTopicConfigAll(topic, defaultMQAdminExt); + Assert.assertEquals(BROKER_NUM, remoteBrokerConfigMap.size()); + for (Map.Entry entry: remoteBrokerConfigMap.entrySet()) { + String broker = entry.getKey(); + TopicConfigAndQueueMapping configMapping = entry.getValue(); + TopicConfigAndQueueMapping localConfigMapping = localBrokerConfigMap.get(broker); + Assert.assertNotNull(localConfigMapping); + Assert.assertEquals(configMapping, localConfigMapping); + } + TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, remoteBrokerConfigMap); + Map globalIdMap = TopicQueueMappingUtils.checkAndBuildMappingItems(new ArrayList<>(getMappingDetailFromConfig(remoteBrokerConfigMap.values())), false, true); + Assert.assertEquals(queueNum, globalIdMap.size()); + } + //send and check + sendMessagesAndCheck(producer, getBrokers(), topic, queueNum, msgEachQueue, 0); + //consume and check + consumeMessagesAndCheck(producer, consumer, topic, queueNum, msgEachQueue, 0, 1); + } + + + @Test + public void testRemappingProduceConsumeStaticTopic() throws Exception { + String topic = "static" + MQRandomUtils.getRandomTopic(); + RMQNormalProducer producer = getProducer(NAMESRV_ADDR, topic); + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener()); + + int queueNum = 1; + int msgEachQueue = 10; + //create send consume + { + Set targetBrokers = ImmutableSet.of(BROKER1_NAME); + MQAdminTestUtils.createStaticTopic(topic, queueNum, targetBrokers, defaultMQAdminExt); + sendMessagesAndCheck(producer, targetBrokers, topic, queueNum, msgEachQueue, 0); + consumeMessagesAndCheck(producer, consumer, topic, queueNum, msgEachQueue, 0, 1); + } + //remapping the static topic + { + Set targetBrokers = ImmutableSet.of(BROKER2_NAME); + MQAdminTestUtils.remappingStaticTopic(topic, targetBrokers, defaultMQAdminExt); + Map remoteBrokerConfigMap = MQAdminUtils.examineTopicConfigAll(topic, defaultMQAdminExt); + TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, remoteBrokerConfigMap); + Map globalIdMap = TopicQueueMappingUtils.checkAndBuildMappingItems(new ArrayList<>(getMappingDetailFromConfig(remoteBrokerConfigMap.values())), false, true); + Assert.assertEquals(queueNum, globalIdMap.size()); + for (TopicQueueMappingOne mappingOne: globalIdMap.values()) { + Assert.assertEquals(BROKER2_NAME, mappingOne.getBname()); + Assert.assertEquals(TopicQueueMappingUtils.DEFAULT_BLOCK_SEQ_SIZE, mappingOne.getItems().get(mappingOne.getItems().size() - 1).getLogicOffset()); + } + awaitRefreshStaticTopicMetadata(3000, topic, producer.getProducer(), consumer.getConsumer(), defaultMQAdminExt); + sendMessagesAndCheck(producer, targetBrokers, topic, queueNum, msgEachQueue, TopicQueueMappingUtils.DEFAULT_BLOCK_SEQ_SIZE); + consumeMessagesAndCheck(producer, consumer, topic, queueNum, msgEachQueue, 0, 2); + } + } + + + public boolean awaitRefreshStaticTopicMetadata(long timeMs, String topic, DefaultMQProducer producer, DefaultMQPushConsumer consumer, DefaultMQAdminExt adminExt) throws Exception { + long start = System.currentTimeMillis(); + MQClientInstance currentInstance = null; + while (System.currentTimeMillis() - start <= timeMs) { + boolean allOk = true; + if (producer != null) { + currentInstance = producer.getDefaultMQProducerImpl().getmQClientFactory(); + currentInstance.updateTopicRouteInfoFromNameServer(topic); + if (!MQAdminTestUtils.checkStaticTopic(topic, adminExt, currentInstance)) { + allOk = false; + } + } + if (consumer != null) { + currentInstance = consumer.getDefaultMQPushConsumerImpl().getmQClientFactory(); + currentInstance.updateTopicRouteInfoFromNameServer(topic); + if (!MQAdminTestUtils.checkStaticTopic(topic, adminExt, currentInstance)) { + allOk = false; + } + } + if (adminExt != null) { + currentInstance = adminExt.getDefaultMQAdminExtImpl().getMqClientInstance(); + currentInstance.updateTopicRouteInfoFromNameServer(topic); + if (!MQAdminTestUtils.checkStaticTopic(topic, adminExt, currentInstance)) { + allOk = false; + } + } + if (allOk) { + return true; + } + Thread.sleep(100); + } + return false; + } + + + @Test + public void testDoubleReadCheckConsumerOffset() throws Exception { + String topic = "static" + MQRandomUtils.getRandomTopic(); + String group = initConsumerGroup(); + RMQNormalProducer producer = getProducer(NAMESRV_ADDR, topic); + RMQNormalConsumer consumer = getConsumer(NAMESRV_ADDR, group, topic, "*", new RMQNormalListener()); + long start = System.currentTimeMillis(); + + int queueNum = 5; + int msgEachQueue = 10; + //create static topic + { + Set targetBrokers = ImmutableSet.of(BROKER1_NAME); + MQAdminTestUtils.createStaticTopic(topic, queueNum, targetBrokers, defaultMQAdminExt); + sendMessagesAndCheck(producer, targetBrokers, topic, queueNum, msgEachQueue, 0); + consumeMessagesAndCheck(producer, consumer, topic, queueNum, msgEachQueue, 0, 1); + } + producer.shutdown(); + consumer.shutdown(); + //use a new producer + producer = getProducer(NAMESRV_ADDR, topic); + + ConsumeStats consumeStats = defaultMQAdminExt.examineConsumeStats(group); + List messageQueues = producer.getMessageQueue(); + for (MessageQueue queue: messageQueues) { + OffsetWrapper wrapper = consumeStats.getOffsetTable().get(queue); + Assert.assertNotNull(wrapper); + Assert.assertEquals(msgEachQueue, wrapper.getBrokerOffset()); + Assert.assertEquals(msgEachQueue, wrapper.getConsumerOffset()); + Assert.assertTrue(wrapper.getLastTimestamp() > start); + } + + List brokers = ImmutableList.of(BROKER2_NAME, BROKER3_NAME, BROKER1_NAME); + for (int i = 0; i < brokers.size(); i++) { + Set targetBrokers = ImmutableSet.of(brokers.get(i)); + MQAdminTestUtils.remappingStaticTopic(topic, targetBrokers, defaultMQAdminExt); + //make the metadata + awaitRefreshStaticTopicMetadata(3000, topic, producer.getProducer(), null, defaultMQAdminExt); + sendMessagesAndCheck(producer, targetBrokers, topic, queueNum, msgEachQueue, (i + 1) * TopicQueueMappingUtils.DEFAULT_BLOCK_SEQ_SIZE); + } + + TestUtils.waitForSeconds(1); + consumeStats = defaultMQAdminExt.examineConsumeStats(group); + + messageQueues = producer.getMessageQueue(); + for (MessageQueue queue: messageQueues) { + OffsetWrapper wrapper = consumeStats.getOffsetTable().get(queue); + Assert.assertNotNull(wrapper); + Assert.assertEquals(msgEachQueue + brokers.size() * TopicQueueMappingUtils.DEFAULT_BLOCK_SEQ_SIZE, wrapper.getBrokerOffset()); + Assert.assertEquals(msgEachQueue, wrapper.getConsumerOffset()); + Assert.assertTrue(wrapper.getLastTimestamp() > start); + } + consumer = getConsumer(NAMESRV_ADDR, group, topic, "*", new RMQNormalListener()); + consumeMessagesAndCheck(producer, consumer, topic, queueNum, msgEachQueue, 1, brokers.size()); + } + + + + + @Test + public void testRemappingAndClear() throws Exception { + String topic = "static" + MQRandomUtils.getRandomTopic(); + RMQNormalProducer producer = getProducer(NAMESRV_ADDR, topic); + int queueNum = 10; + int msgEachQueue = 100; + //create to broker1Name + { + Set targetBrokers = ImmutableSet.of(BROKER1_NAME); + MQAdminTestUtils.createStaticTopic(topic, queueNum, targetBrokers, defaultMQAdminExt); + //leave the time to refresh the metadata + awaitRefreshStaticTopicMetadata(3000, topic, producer.getProducer(), null, defaultMQAdminExt); + sendMessagesAndCheck(producer, targetBrokers, topic, queueNum, msgEachQueue, 0); + } + + //remapping to broker2Name + { + Set targetBrokers = ImmutableSet.of(BROKER2_NAME); + MQAdminTestUtils.remappingStaticTopic(topic, targetBrokers, defaultMQAdminExt); + //leave the time to refresh the metadata + awaitRefreshStaticTopicMetadata(3000, topic, producer.getProducer(), null, defaultMQAdminExt); + sendMessagesAndCheck(producer, targetBrokers, topic, queueNum, msgEachQueue, 1 * TopicQueueMappingUtils.DEFAULT_BLOCK_SEQ_SIZE); + } + + //remapping to broker3Name + { + Set targetBrokers = ImmutableSet.of(BROKER3_NAME); + MQAdminTestUtils.remappingStaticTopic(topic, targetBrokers, defaultMQAdminExt); + //leave the time to refresh the metadata + awaitRefreshStaticTopicMetadata(3000, topic, producer.getProducer(), null, defaultMQAdminExt); + sendMessagesAndCheck(producer, targetBrokers, topic, queueNum, msgEachQueue, 2 * TopicQueueMappingUtils.DEFAULT_BLOCK_SEQ_SIZE); + } + + // 1 -> 2 -> 3, currently 1 should not have any mappings + + { + for (int i = 0; i < 10; i++) { + for (BrokerController brokerController: brokerControllerList) { + brokerController.getTopicQueueMappingCleanService().wakeup(); + } + Thread.sleep(100); + } + Map brokerConfigMap = MQAdminUtils.examineTopicConfigAll(topic, defaultMQAdminExt); + Assert.assertEquals(BROKER_NUM, brokerConfigMap.size()); + TopicConfigAndQueueMapping config1 = brokerConfigMap.get(BROKER1_NAME); + TopicConfigAndQueueMapping config2 = brokerConfigMap.get(BROKER2_NAME); + TopicConfigAndQueueMapping config3 = brokerConfigMap.get(BROKER3_NAME); + Assert.assertEquals(0, config1.getMappingDetail().getHostedQueues().size()); + Assert.assertEquals(queueNum, config2.getMappingDetail().getHostedQueues().size()); + + Assert.assertEquals(queueNum, config3.getMappingDetail().getHostedQueues().size()); + + } + { + Set topics = new HashSet<>(brokerController1.getTopicConfigManager().getTopicConfigTable().keySet()); + topics.remove(topic); + brokerController1.getMessageStore().cleanUnusedTopic(topics); + brokerController2.getMessageStore().cleanUnusedTopic(topics); + for (int i = 0; i < 10; i++) { + for (BrokerController brokerController: brokerControllerList) { + brokerController.getTopicQueueMappingCleanService().wakeup(); + } + Thread.sleep(100); + } + + Map brokerConfigMap = MQAdminUtils.examineTopicConfigAll(topic, defaultMQAdminExt); + Assert.assertEquals(BROKER_NUM, brokerConfigMap.size()); + TopicConfigAndQueueMapping config1 = brokerConfigMap.get(BROKER1_NAME); + TopicConfigAndQueueMapping config2 = brokerConfigMap.get(BROKER2_NAME); + TopicConfigAndQueueMapping config3 = brokerConfigMap.get(BROKER3_NAME); + Assert.assertEquals(0, config1.getMappingDetail().getHostedQueues().size()); + Assert.assertEquals(queueNum, config2.getMappingDetail().getHostedQueues().size()); + Assert.assertEquals(queueNum, config3.getMappingDetail().getHostedQueues().size()); + //The first leader will clear it + for (List items : config1.getMappingDetail().getHostedQueues().values()) { + Assert.assertEquals(3, items.size()); + } + //The second leader do nothing + for (List items : config3.getMappingDetail().getHostedQueues().values()) { + Assert.assertEquals(1, items.size()); + } + } + } + + + @Test + public void testRemappingWithNegativeLogicOffset() throws Exception { + String topic = "static" + MQRandomUtils.getRandomTopic(); + RMQNormalProducer producer = getProducer(NAMESRV_ADDR, topic); + int queueNum = 10; + int msgEachQueue = 100; + //create and send + { + Set targetBrokers = ImmutableSet.of(BROKER1_NAME); + MQAdminTestUtils.createStaticTopic(topic, queueNum, targetBrokers, defaultMQAdminExt); + sendMessagesAndCheck(producer, targetBrokers, topic, queueNum, msgEachQueue, 0); + } + + //remapping the static topic with -1 logic offset + { + Set targetBrokers = ImmutableSet.of(BROKER2_NAME); + MQAdminTestUtils.remappingStaticTopicWithNegativeLogicOffset(topic, targetBrokers, defaultMQAdminExt); + Map remoteBrokerConfigMap = MQAdminUtils.examineTopicConfigAll(topic, defaultMQAdminExt); + TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, remoteBrokerConfigMap); + Map globalIdMap = TopicQueueMappingUtils.checkAndBuildMappingItems(new ArrayList<>(getMappingDetailFromConfig(remoteBrokerConfigMap.values())), false, true); + Assert.assertEquals(queueNum, globalIdMap.size()); + for (TopicQueueMappingOne mappingOne: globalIdMap.values()) { + Assert.assertEquals(BROKER2_NAME, mappingOne.getBname()); + Assert.assertEquals(-1, mappingOne.getItems().get(mappingOne.getItems().size() - 1).getLogicOffset()); + } + //leave the time to refresh the metadata + awaitRefreshStaticTopicMetadata(3000, topic, producer.getProducer(), null, defaultMQAdminExt); + //here the gen should be 0 + sendMessagesAndCheck(producer, targetBrokers, topic, queueNum, msgEachQueue, 0); + } + } + + + @After + public void tearDown() { + System.setProperty("rocketmq.client.rebalance.waitInterval", "20000"); + super.shutdown(); + } + +} diff --git a/test/src/test/java/org/apache/rocketmq/test/tls/TlsIT.java b/test/src/test/java/org/apache/rocketmq/test/tls/TlsIT.java new file mode 100644 index 0000000..4cddaa8 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/tls/TlsIT.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.test.tls; + +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.MQWait; +import org.assertj.core.api.Assertions; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class TlsIT extends BaseConf { + + private RMQNormalProducer producer; + private RMQNormalConsumer consumer; + + private String topic; + + @Before + public void setUp() { + topic = initTopic(); + // Send messages via TLS + producer = getProducer(NAMESRV_ADDR, topic, true); + // Receive messages via TLS + consumer = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener(), true); + } + + @After + public void tearDown() { + shutdown(); + } + + @Test + public void testSendAndReceiveMessageOverTLS() { + int numberOfMessagesToSend = 16; + producer.send(numberOfMessagesToSend); + + boolean consumedAll = MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), consumer.getListener()); + Assertions.assertThat(consumedAll).isEqualTo(true); + } + +} diff --git a/test/src/test/java/org/apache/rocketmq/test/tls/TlsMix2IT.java b/test/src/test/java/org/apache/rocketmq/test/tls/TlsMix2IT.java new file mode 100644 index 0000000..01350e8 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/tls/TlsMix2IT.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.test.tls; + +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.MQWait; +import org.assertj.core.api.Assertions; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class TlsMix2IT extends BaseConf { + + private RMQNormalProducer producer; + private RMQNormalConsumer consumer; + + private String topic; + + @Before + public void setUp() { + topic = initTopic(); + // send message via TLS + producer = getProducer(NAMESRV_ADDR, topic, true); + + // Receive message without TLS. + consumer = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener(), false); + } + + @After + public void tearDown() { + shutdown(); + } + + @Test + public void testSendAndReceiveMessageOverTLS() { + int numberOfMessagesToSend = 16; + producer.send(numberOfMessagesToSend); + + boolean consumedAll = MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), consumer.getListener()); + Assertions.assertThat(consumedAll).isEqualTo(true); + } + +} diff --git a/test/src/test/java/org/apache/rocketmq/test/tls/TlsMixIT.java b/test/src/test/java/org/apache/rocketmq/test/tls/TlsMixIT.java new file mode 100644 index 0000000..33b49b7 --- /dev/null +++ b/test/src/test/java/org/apache/rocketmq/test/tls/TlsMixIT.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.test.tls; + +import org.apache.rocketmq.test.base.BaseConf; +import org.apache.rocketmq.test.client.rmq.RMQNormalConsumer; +import org.apache.rocketmq.test.client.rmq.RMQNormalProducer; +import org.apache.rocketmq.test.listener.rmq.concurrent.RMQNormalListener; +import org.apache.rocketmq.test.util.MQWait; +import org.assertj.core.api.Assertions; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class TlsMixIT extends BaseConf { + + private RMQNormalProducer producer; + private RMQNormalConsumer consumer; + + private String topic; + + @Before + public void setUp() { + topic = initTopic(); + + // send message without TLS + producer = getProducer(NAMESRV_ADDR, topic); + + // Receive message via TLS + consumer = getConsumer(NAMESRV_ADDR, topic, "*", new RMQNormalListener(), true); + } + + @After + public void tearDown() { + shutdown(); + } + + @Test + public void testSendAndReceiveMessageOverTLS() { + int numberOfMessagesToSend = 16; + producer.send(numberOfMessagesToSend); + + boolean consumedAll = MQWait.waitConsumeAll(CONSUME_TIME, producer.getAllMsgBody(), consumer.getListener()); + Assertions.assertThat(consumedAll).isEqualTo(true); + } + +} diff --git a/test/src/test/resources/rmq-proxy-home/conf/broker.conf b/test/src/test/resources/rmq-proxy-home/conf/broker.conf new file mode 100644 index 0000000..0c0b28b --- /dev/null +++ b/test/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/test/src/test/resources/rmq-proxy-home/conf/logback_proxy.xml b/test/src/test/resources/rmq-proxy-home/conf/logback_proxy.xml new file mode 100644 index 0000000..9019996 --- /dev/null +++ b/test/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 + 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}grpc.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}grpc.%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}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}log${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}qpop.%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/test/src/test/resources/rmq-proxy-home/conf/rmq-proxy.json b/test/src/test/resources/rmq-proxy-home/conf/rmq-proxy.json new file mode 100644 index 0000000..f0873e2 --- /dev/null +++ b/test/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/test/src/test/resources/rmq.logback-test.xml b/test/src/test/resources/rmq.logback-test.xml new file mode 100644 index 0000000..8695d52 --- /dev/null +++ b/test/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/test/src/test/resources/schema/api/client.consumer.AllocateMessageQueueStrategy.schema b/test/src/test/resources/schema/api/client.consumer.AllocateMessageQueueStrategy.schema new file mode 100644 index 0000000..7f23cab --- /dev/null +++ b/test/src/test/resources/schema/api/client.consumer.AllocateMessageQueueStrategy.schema @@ -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. + */ + + +Method allocate(java.lang.String,java.lang.String,java.util.List,java.util.List) : public throws (java.util.List) +Method getName() : public throws (java.lang.String) diff --git a/test/src/test/resources/schema/api/client.consumer.DefaultLitePullConsumer.schema b/test/src/test/resources/schema/api/client.consumer.DefaultLitePullConsumer.schema new file mode 100644 index 0000000..79cd6c1 --- /dev/null +++ b/test/src/test/resources/schema/api/client.consumer.DefaultLitePullConsumer.schema @@ -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. + */ + + +Field MIN_AUTOCOMMIT_INTERVAL_MILLIS : private long 1000 +Field SEND_MESSAGE_WITH_VIP_CHANNEL_PROPERTY : public java.lang.String com.rocketmq.sendMessageWithVIPChannel +Field accessChannel : protected org.apache.rocketmq.client.AccessChannel LOCAL +Field allocateMessageQueueStrategy : private org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy null +Field autoCommit : private boolean true +Field autoCommitIntervalMillis : private long 5000 +Field brokerSuspendMaxTimeMillis : private long 20000 +Field clientCallbackExecutorThreads : private int null +Field clientIP : private java.lang.String null +Field consumeFromWhere : private org.apache.rocketmq.common.consumer.ConsumeFromWhere CONSUME_FROM_LAST_OFFSET +Field consumeMaxSpan : private int 2000 +Field consumeTimestamp : private java.lang.String null +Field consumerGroup : private java.lang.String DEFAULT_CONSUMER +Field consumerPullTimeoutMillis : private long 10000 +Field consumerTimeoutMillisWhenSuspend : private long 30000 +Field customizedTraceTopic : private java.lang.String null +Field defaultLitePullConsumerImpl : private org.apache.rocketmq.client.impl.consumer.DefaultLitePullConsumerImpl null +Field enableMsgTrace : private boolean false +Field enableStreamRequestType : protected boolean true +Field heartbeatBrokerInterval : private int 30000 +Field instanceName : private java.lang.String DEFAULT +Field language : private org.apache.rocketmq.remoting.protocol.LanguageCode JAVA +Field log : private org.apache.rocketmq.logging.InternalLogger null +Field messageModel : private org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel CLUSTERING +Field messageQueueListener : private org.apache.rocketmq.client.consumer.MessageQueueListener null +Field mqClientApiTimeout : private int 3000 +Field namespace : protected java.lang.String null +Field namespaceInitialized : private boolean false +Field namesrvAddr : private java.lang.String null +Field offsetStore : private org.apache.rocketmq.client.consumer.store.OffsetStore null +Field persistConsumerOffsetInterval : private int 5000 +Field pollNameServerInterval : private int 30000 +Field pollTimeoutMillis : private long 5000 +Field pullBatchSize : private int 10 +Field pullThreadNums : private int 20 +Field pullThresholdForAll : private long 10000 +Field pullThresholdForQueue : private int 1000 +Field pullThresholdSizeForQueue : private int 100 +Field pullTimeDelayMillsWhenException : private long 1000 +Field topicMetadataCheckIntervalMillis : private long 30000 +Field traceDispatcher : private org.apache.rocketmq.client.trace.TraceDispatcher null +Field unitMode : private boolean false +Field unitName : private java.lang.String null +Field useTLS : private boolean false +Field vipChannelEnabled : private boolean false +Method assign(java.util.Collection) : public throws (void) +Method buildMQClientId() : public throws (java.lang.String) +Method changeInstanceNameToPID() : public throws (void) +Method cloneClientConfig() : public throws (org.apache.rocketmq.client.ClientConfig) +Method commit(boolean,java.util.Set) : public throws (void) +Method commitSync() : public throws (void) +Method committed(org.apache.rocketmq.common.message.MessageQueue) : public throws (java.lang.Long) +Method fetchMessageQueues(java.lang.String) : public throws (java.util.Collection) +Method getAccessChannel() : public throws (org.apache.rocketmq.client.AccessChannel) +Method getAllocateMessageQueueStrategy() : public throws (org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy) +Method getAutoCommitIntervalMillis() : public throws (long) +Method getBrokerSuspendMaxTimeMillis() : public throws (long) +Method getClientCallbackExecutorThreads() : public throws (int) +Method getClientIP() : public throws (java.lang.String) +Method getConsumeFromWhere() : public throws (org.apache.rocketmq.common.consumer.ConsumeFromWhere) +Method getConsumeMaxSpan() : public throws (int) +Method getConsumeTimestamp() : public throws (java.lang.String) +Method getConsumerGroup() : public throws (java.lang.String) +Method getConsumerPullTimeoutMillis() : public throws (long) +Method getConsumerTimeoutMillisWhenSuspend() : public throws (long) +Method getCustomizedTraceTopic() : public throws (java.lang.String) +Method getDefaultBrokerId() : public throws (long) +Method getHeartbeatBrokerInterval() : public throws (int) +Method getInstanceName() : public throws (java.lang.String) +Method getLanguage() : public throws (org.apache.rocketmq.remoting.protocol.LanguageCode) +Method getMessageModel() : public throws (org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel) +Method getMessageQueueListener() : public throws (org.apache.rocketmq.client.consumer.MessageQueueListener) +Method getMqClientApiTimeout() : public throws (int) +Method getNamespace() : public throws (java.lang.String) +Method getNamesrvAddr() : public throws (java.lang.String) +Method getOffsetStore() : public throws (org.apache.rocketmq.client.consumer.store.OffsetStore) +Method getPersistConsumerOffsetInterval() : public throws (int) +Method getPollNameServerInterval() : public throws (int) +Method getPollTimeoutMillis() : public throws (long) +Method getPullBatchSize() : public throws (int) +Method getPullThreadNums() : public throws (int) +Method getPullThresholdForAll() : public throws (long) +Method getPullThresholdForQueue() : public throws (int) +Method getPullThresholdSizeForQueue() : public throws (int) +Method getPullTimeDelayMillsWhenException() : public throws (long) +Method getTopicMetadataCheckIntervalMillis() : public throws (long) +Method getTraceDispatcher() : public throws (org.apache.rocketmq.client.trace.TraceDispatcher) +Method getUnitName() : public throws (java.lang.String) +Method isAutoCommit() : public throws (boolean) +Method isConnectBrokerByUser() : public throws (boolean) +Method isEnableMsgTrace() : public throws (boolean) +Method isEnableStreamRequestType() : public throws (boolean) +Method isRunning() : public throws (boolean) +Method isUnitMode() : public throws (boolean) +Method isUseTLS() : public throws (boolean) +Method isVipChannelEnabled() : public throws (boolean) +Method offsetForTimestamp(java.lang.Long,org.apache.rocketmq.common.message.MessageQueue) : public throws (java.lang.Long) +Method pause(java.util.Collection) : public throws (void) +Method poll() : public throws (java.util.List) +Method poll(long) : public throws (java.util.List) +Method queueWithNamespace(org.apache.rocketmq.common.message.MessageQueue) : public throws (org.apache.rocketmq.common.message.MessageQueue) +Method queuesWithNamespace(java.util.Collection) : public throws (java.util.Collection) +Method registerTopicMessageQueueChangeListener(java.lang.String,org.apache.rocketmq.client.consumer.TopicMessageQueueChangeListener) : public throws (void) +Method resetClientConfig(org.apache.rocketmq.client.ClientConfig) : public throws (void) +Method resume(java.util.Collection) : public throws (void) +Method seek(long,org.apache.rocketmq.common.message.MessageQueue) : public throws (void) +Method seekToBegin(org.apache.rocketmq.common.message.MessageQueue) : public throws (void) +Method seekToEnd(org.apache.rocketmq.common.message.MessageQueue) : public throws (void) +Method setAccessChannel(org.apache.rocketmq.client.AccessChannel) : public throws (void) +Method setAllocateMessageQueueStrategy(org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy) : public throws (void) +Method setAutoCommit(boolean) : public throws (void) +Method setAutoCommitIntervalMillis(long) : public throws (void) +Method setClientCallbackExecutorThreads(int) : public throws (void) +Method setClientIP(java.lang.String) : public throws (void) +Method setConnectBrokerByUser(boolean) : public throws (void) +Method setConsumeFromWhere(org.apache.rocketmq.common.consumer.ConsumeFromWhere) : public throws (void) +Method setConsumeMaxSpan(int) : public throws (void) +Method setConsumeTimestamp(java.lang.String) : public throws (void) +Method setConsumerGroup(java.lang.String) : public throws (void) +Method setConsumerPullTimeoutMillis(long) : public throws (void) +Method setConsumerTimeoutMillisWhenSuspend(long) : public throws (void) +Method setCustomizedTraceTopic(java.lang.String) : public throws (void) +Method setDefaultBrokerId(long) : public throws (void) +Method setEnableMsgTrace(boolean) : public throws (void) +Method setEnableStreamRequestType(boolean) : public throws (void) +Method setHeartbeatBrokerInterval(int) : public throws (void) +Method setInstanceName(java.lang.String) : public throws (void) +Method setLanguage(org.apache.rocketmq.remoting.protocol.LanguageCode) : public throws (void) +Method setMessageModel(org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel) : public throws (void) +Method setMessageQueueListener(org.apache.rocketmq.client.consumer.MessageQueueListener) : public throws (void) +Method setMqClientApiTimeout(int) : public throws (void) +Method setNamespace(java.lang.String) : public throws (void) +Method setNamesrvAddr(java.lang.String) : public throws (void) +Method setOffsetStore(org.apache.rocketmq.client.consumer.store.OffsetStore) : public throws (void) +Method setPersistConsumerOffsetInterval(int) : public throws (void) +Method setPollNameServerInterval(int) : public throws (void) +Method setPollTimeoutMillis(long) : public throws (void) +Method setPullBatchSize(int) : public throws (void) +Method setPullThreadNums(int) : public throws (void) +Method setPullThresholdForAll(long) : public throws (void) +Method setPullThresholdForQueue(int) : public throws (void) +Method setPullThresholdSizeForQueue(int) : public throws (void) +Method setPullTimeDelayMillsWhenException(long) : public throws (void) +Method setTopicMetadataCheckIntervalMillis(long) : public throws (void) +Method setUnitMode(boolean) : public throws (void) +Method setUnitName(java.lang.String) : public throws (void) +Method setUseTLS(boolean) : public throws (void) +Method setVipChannelEnabled(boolean) : public throws (void) +Method shutdown() : public throws (void) +Method start() : public throws (void) +Method subscribe(java.lang.String,java.lang.String) : public throws (void) +Method subscribe(java.lang.String,org.apache.rocketmq.client.consumer.MessageSelector) : public throws (void) +Method toString() : public throws (java.lang.String) +Method unsubscribe(java.lang.String) : public throws (void) +Method updateNameServerAddress(java.lang.String) : public throws (void) +Method withNamespace(java.lang.String) : public throws (java.lang.String) +Method withNamespace(java.util.Set) : public throws (java.util.Set) +Method withoutNamespace(java.lang.String) : public throws (java.lang.String) +Method withoutNamespace(java.util.Set) : public throws (java.util.Set) diff --git a/test/src/test/resources/schema/api/client.consumer.DefaultMQPullConsumer.schema b/test/src/test/resources/schema/api/client.consumer.DefaultMQPullConsumer.schema new file mode 100644 index 0000000..92d4cba --- /dev/null +++ b/test/src/test/resources/schema/api/client.consumer.DefaultMQPullConsumer.schema @@ -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. + */ + + +Field SEND_MESSAGE_WITH_VIP_CHANNEL_PROPERTY : public java.lang.String com.rocketmq.sendMessageWithVIPChannel +Field accessChannel : protected org.apache.rocketmq.client.AccessChannel LOCAL +Field allocateMessageQueueStrategy : private org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy null +Field brokerSuspendMaxTimeMillis : private long 20000 +Field clientCallbackExecutorThreads : private int null +Field clientIP : private java.lang.String null +Field consumerGroup : private java.lang.String DEFAULT_CONSUMER +Field consumerPullTimeoutMillis : private long 10000 +Field consumerTimeoutMillisWhenSuspend : private long 30000 +Field defaultMQPullConsumerImpl : protected org.apache.rocketmq.client.impl.consumer.DefaultMQPullConsumerImpl null +Field enableStreamRequestType : protected boolean true +Field heartbeatBrokerInterval : private int 30000 +Field instanceName : private java.lang.String DEFAULT +Field language : private org.apache.rocketmq.remoting.protocol.LanguageCode JAVA +Field maxReconsumeTimes : private int 16 +Field messageModel : private org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel CLUSTERING +Field messageQueueListener : private org.apache.rocketmq.client.consumer.MessageQueueListener null +Field mqClientApiTimeout : private int 3000 +Field namespace : protected java.lang.String null +Field namespaceInitialized : private boolean false +Field namesrvAddr : private java.lang.String null +Field offsetStore : private org.apache.rocketmq.client.consumer.store.OffsetStore null +Field persistConsumerOffsetInterval : private int 5000 +Field pollNameServerInterval : private int 30000 +Field pullTimeDelayMillsWhenException : private long 1000 +Field registerTopics : private java.util.Set null +Field unitMode : private boolean false +Field unitName : private java.lang.String null +Field useTLS : private boolean false +Field vipChannelEnabled : private boolean false +Method buildMQClientId() : public throws (java.lang.String) +Method changeInstanceNameToPID() : public throws (void) +Method cloneClientConfig() : public throws (org.apache.rocketmq.client.ClientConfig) +Method createTopic(int,int,java.lang.String,java.lang.String) : public throws (void) +Method createTopic(int,java.lang.String,java.lang.String) : public throws (void) +Method earliestMsgStoreTime(org.apache.rocketmq.common.message.MessageQueue) : public throws (long) +Method fetchConsumeOffset(boolean,org.apache.rocketmq.common.message.MessageQueue) : public throws (long) +Method fetchMessageQueuesInBalance(java.lang.String) : public throws (java.util.Set) +Method fetchSubscribeMessageQueues(java.lang.String) : public throws (java.util.Set) +Method getAccessChannel() : public throws (org.apache.rocketmq.client.AccessChannel) +Method getAllocateMessageQueueStrategy() : public throws (org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy) +Method getBrokerSuspendMaxTimeMillis() : public throws (long) +Method getClientCallbackExecutorThreads() : public throws (int) +Method getClientIP() : public throws (java.lang.String) +Method getConsumerGroup() : public throws (java.lang.String) +Method getConsumerPullTimeoutMillis() : public throws (long) +Method getConsumerTimeoutMillisWhenSuspend() : public throws (long) +Method getDefaultMQPullConsumerImpl() : public throws (org.apache.rocketmq.client.impl.consumer.DefaultMQPullConsumerImpl) +Method getHeartbeatBrokerInterval() : public throws (int) +Method getInstanceName() : public throws (java.lang.String) +Method getLanguage() : public throws (org.apache.rocketmq.remoting.protocol.LanguageCode) +Method getMaxReconsumeTimes() : public throws (int) +Method getMessageModel() : public throws (org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel) +Method getMessageQueueListener() : public throws (org.apache.rocketmq.client.consumer.MessageQueueListener) +Method getMqClientApiTimeout() : public throws (int) +Method getNamespace() : public throws (java.lang.String) +Method getNamesrvAddr() : public throws (java.lang.String) +Method getOffsetStore() : public throws (org.apache.rocketmq.client.consumer.store.OffsetStore) +Method getPersistConsumerOffsetInterval() : public throws (int) +Method getPollNameServerInterval() : public throws (int) +Method getPullTimeDelayMillsWhenException() : public throws (long) +Method getRegisterTopics() : public throws (java.util.Set) +Method getUnitName() : public throws (java.lang.String) +Method isEnableStreamRequestType() : public throws (boolean) +Method isUnitMode() : public throws (boolean) +Method isUseTLS() : public throws (boolean) +Method isVipChannelEnabled() : public throws (boolean) +Method maxOffset(org.apache.rocketmq.common.message.MessageQueue) : public throws (long) +Method minOffset(org.apache.rocketmq.common.message.MessageQueue) : public throws (long) +Method pull(int,java.lang.String,long,long,org.apache.rocketmq.client.consumer.PullCallback,org.apache.rocketmq.common.message.MessageQueue) : public throws (void) +Method pull(int,java.lang.String,long,long,org.apache.rocketmq.common.message.MessageQueue) : public throws (org.apache.rocketmq.client.consumer.PullResult) +Method pull(int,java.lang.String,long,org.apache.rocketmq.client.consumer.PullCallback,org.apache.rocketmq.common.message.MessageQueue) : public throws (void) +Method pull(int,java.lang.String,long,org.apache.rocketmq.common.message.MessageQueue) : public throws (org.apache.rocketmq.client.consumer.PullResult) +Method pull(int,long,long,org.apache.rocketmq.client.consumer.MessageSelector,org.apache.rocketmq.client.consumer.PullCallback,org.apache.rocketmq.common.message.MessageQueue) : public throws (void) +Method pull(int,long,long,org.apache.rocketmq.client.consumer.MessageSelector,org.apache.rocketmq.common.message.MessageQueue) : public throws (org.apache.rocketmq.client.consumer.PullResult) +Method pull(int,long,org.apache.rocketmq.client.consumer.MessageSelector,org.apache.rocketmq.client.consumer.PullCallback,org.apache.rocketmq.common.message.MessageQueue) : public throws (void) +Method pull(int,long,org.apache.rocketmq.client.consumer.MessageSelector,org.apache.rocketmq.common.message.MessageQueue) : public throws (org.apache.rocketmq.client.consumer.PullResult) +Method pullBlockIfNotFound(int,java.lang.String,long,org.apache.rocketmq.client.consumer.PullCallback,org.apache.rocketmq.common.message.MessageQueue) : public throws (void) +Method pullBlockIfNotFound(int,java.lang.String,long,org.apache.rocketmq.common.message.MessageQueue) : public throws (org.apache.rocketmq.client.consumer.PullResult) +Method queryMessage(int,java.lang.String,java.lang.String,long,long) : public throws (org.apache.rocketmq.client.QueryResult) +Method queueWithNamespace(org.apache.rocketmq.common.message.MessageQueue) : public throws (org.apache.rocketmq.common.message.MessageQueue) +Method queuesWithNamespace(java.util.Collection) : public throws (java.util.Collection) +Method registerMessageQueueListener(java.lang.String,org.apache.rocketmq.client.consumer.MessageQueueListener) : public throws (void) +Method resetClientConfig(org.apache.rocketmq.client.ClientConfig) : public throws (void) +Method searchOffset(long,org.apache.rocketmq.common.message.MessageQueue) : public throws (long) +Method sendMessageBack(int,java.lang.String,java.lang.String,org.apache.rocketmq.common.message.MessageExt) : public throws (void) +Method sendMessageBack(int,java.lang.String,org.apache.rocketmq.common.message.MessageExt) : public throws (void) +Method sendMessageBack(int,org.apache.rocketmq.common.message.MessageExt) : public throws (void) +Method setAccessChannel(org.apache.rocketmq.client.AccessChannel) : public throws (void) +Method setAllocateMessageQueueStrategy(org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy) : public throws (void) +Method setBrokerSuspendMaxTimeMillis(long) : public throws (void) +Method setClientCallbackExecutorThreads(int) : public throws (void) +Method setClientIP(java.lang.String) : public throws (void) +Method setConsumerGroup(java.lang.String) : public throws (void) +Method setConsumerPullTimeoutMillis(long) : public throws (void) +Method setConsumerTimeoutMillisWhenSuspend(long) : public throws (void) +Method setEnableStreamRequestType(boolean) : public throws (void) +Method setHeartbeatBrokerInterval(int) : public throws (void) +Method setInstanceName(java.lang.String) : public throws (void) +Method setLanguage(org.apache.rocketmq.remoting.protocol.LanguageCode) : public throws (void) +Method setMaxReconsumeTimes(int) : public throws (void) +Method setMessageModel(org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel) : public throws (void) +Method setMessageQueueListener(org.apache.rocketmq.client.consumer.MessageQueueListener) : public throws (void) +Method setMqClientApiTimeout(int) : public throws (void) +Method setNamespace(java.lang.String) : public throws (void) +Method setNamesrvAddr(java.lang.String) : public throws (void) +Method setOffsetStore(org.apache.rocketmq.client.consumer.store.OffsetStore) : public throws (void) +Method setPersistConsumerOffsetInterval(int) : public throws (void) +Method setPollNameServerInterval(int) : public throws (void) +Method setPullTimeDelayMillsWhenException(long) : public throws (void) +Method setRegisterTopics(java.util.Set) : public throws (void) +Method setUnitMode(boolean) : public throws (void) +Method setUnitName(java.lang.String) : public throws (void) +Method setUseTLS(boolean) : public throws (void) +Method setVipChannelEnabled(boolean) : public throws (void) +Method shutdown() : public throws (void) +Method start() : public throws (void) +Method toString() : public throws (java.lang.String) +Method updateConsumeOffset(long,org.apache.rocketmq.common.message.MessageQueue) : public throws (void) +Method viewMessage(java.lang.String) : public throws (org.apache.rocketmq.common.message.MessageExt) +Method viewMessage(java.lang.String,java.lang.String) : public throws (org.apache.rocketmq.common.message.MessageExt) +Method withNamespace(java.lang.String) : public throws (java.lang.String) +Method withNamespace(java.util.Set) : public throws (java.util.Set) +Method withoutNamespace(java.lang.String) : public throws (java.lang.String) +Method withoutNamespace(java.util.Set) : public throws (java.util.Set) diff --git a/test/src/test/resources/schema/api/client.consumer.DefaultMQPushConsumer.schema b/test/src/test/resources/schema/api/client.consumer.DefaultMQPushConsumer.schema new file mode 100644 index 0000000..3aed1d6 --- /dev/null +++ b/test/src/test/resources/schema/api/client.consumer.DefaultMQPushConsumer.schema @@ -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. + */ + + +Field SEND_MESSAGE_WITH_VIP_CHANNEL_PROPERTY : public java.lang.String com.rocketmq.sendMessageWithVIPChannel +Field accessChannel : protected org.apache.rocketmq.client.AccessChannel LOCAL +Field adjustThreadPoolNumsThreshold : private long 100000 +Field allocateMessageQueueStrategy : private org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy null +Field awaitTerminationMillisWhenShutdown : private long 0 +Field clientCallbackExecutorThreads : private int null +Field clientIP : private java.lang.String null +Field consumeConcurrentlyMaxSpan : private int 2000 +Field consumeFromWhere : private org.apache.rocketmq.common.consumer.ConsumeFromWhere CONSUME_FROM_LAST_OFFSET +Field consumeMessageBatchMaxSize : private int 1 +Field consumeThreadMax : private int 20 +Field consumeThreadMin : private int 20 +Field consumeTimeout : private long 15 +Field consumeTimestamp : private java.lang.String null +Field consumerGroup : private java.lang.String DEFAULT_CONSUMER +Field defaultMQPushConsumerImpl : protected org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl null +Field enableStreamRequestType : protected boolean false +Field heartbeatBrokerInterval : private int 30000 +Field instanceName : private java.lang.String DEFAULT +Field language : private org.apache.rocketmq.remoting.protocol.LanguageCode JAVA +Field log : private org.apache.rocketmq.logging.InternalLogger null +Field maxReconsumeTimes : private int -1 +Field messageListener : private org.apache.rocketmq.client.consumer.listener.MessageListener null +Field messageModel : private org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel CLUSTERING +Field mqClientApiTimeout : private int 3000 +Field namespace : protected java.lang.String null +Field namespaceInitialized : private boolean false +Field namesrvAddr : private java.lang.String null +Field offsetStore : private org.apache.rocketmq.client.consumer.store.OffsetStore null +Field persistConsumerOffsetInterval : private int 5000 +Field pollNameServerInterval : private int 30000 +Field postSubscriptionWhenPull : private boolean false +Field pullBatchSize : private int 32 +Field pullInterval : private long 0 +Field pullThresholdForQueue : private int 1000 +Field pullThresholdForTopic : private int -1 +Field pullThresholdSizeForQueue : private int 100 +Field pullThresholdSizeForTopic : private int -1 +Field pullTimeDelayMillsWhenException : private long 1000 +Field subscription : private java.util.Map null +Field suspendCurrentQueueTimeMillis : private long 1000 +Field traceDispatcher : private org.apache.rocketmq.client.trace.TraceDispatcher null +Field unitMode : private boolean false +Field unitName : private java.lang.String null +Field useTLS : private boolean false +Field vipChannelEnabled : private boolean false +Method buildMQClientId() : public throws (java.lang.String) +Method changeInstanceNameToPID() : public throws (void) +Method cloneClientConfig() : public throws (org.apache.rocketmq.client.ClientConfig) +Method createTopic(int,int,java.lang.String,java.lang.String) : public throws (void) +Method createTopic(int,java.lang.String,java.lang.String) : public throws (void) +Method earliestMsgStoreTime(org.apache.rocketmq.common.message.MessageQueue) : public throws (long) +Method fetchSubscribeMessageQueues(java.lang.String) : public throws (java.util.Set) +Method getAccessChannel() : public throws (org.apache.rocketmq.client.AccessChannel) +Method getAdjustThreadPoolNumsThreshold() : public throws (long) +Method getAllocateMessageQueueStrategy() : public throws (org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy) +Method getAwaitTerminationMillisWhenShutdown() : public throws (long) +Method getClientCallbackExecutorThreads() : public throws (int) +Method getClientIP() : public throws (java.lang.String) +Method getConsumeConcurrentlyMaxSpan() : public throws (int) +Method getConsumeFromWhere() : public throws (org.apache.rocketmq.common.consumer.ConsumeFromWhere) +Method getConsumeMessageBatchMaxSize() : public throws (int) +Method getConsumeThreadMax() : public throws (int) +Method getConsumeThreadMin() : public throws (int) +Method getConsumeTimeout() : public throws (long) +Method getConsumeTimestamp() : public throws (java.lang.String) +Method getConsumerGroup() : public throws (java.lang.String) +Method getDefaultMQPushConsumerImpl() : public throws (org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl) +Method getHeartbeatBrokerInterval() : public throws (int) +Method getInstanceName() : public throws (java.lang.String) +Method getLanguage() : public throws (org.apache.rocketmq.remoting.protocol.LanguageCode) +Method getMaxReconsumeTimes() : public throws (int) +Method getMessageListener() : public throws (org.apache.rocketmq.client.consumer.listener.MessageListener) +Method getMessageModel() : public throws (org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel) +Method getMqClientApiTimeout() : public throws (int) +Method getNamespace() : public throws (java.lang.String) +Method getNamesrvAddr() : public throws (java.lang.String) +Method getOffsetStore() : public throws (org.apache.rocketmq.client.consumer.store.OffsetStore) +Method getPersistConsumerOffsetInterval() : public throws (int) +Method getPollNameServerInterval() : public throws (int) +Method getPullBatchSize() : public throws (int) +Method getPullInterval() : public throws (long) +Method getPullThresholdForQueue() : public throws (int) +Method getPullThresholdForTopic() : public throws (int) +Method getPullThresholdSizeForQueue() : public throws (int) +Method getPullThresholdSizeForTopic() : public throws (int) +Method getPullTimeDelayMillsWhenException() : public throws (long) +Method getSubscription() : public throws (java.util.Map) +Method getSuspendCurrentQueueTimeMillis() : public throws (long) +Method getTraceDispatcher() : public throws (org.apache.rocketmq.client.trace.TraceDispatcher) +Method getUnitName() : public throws (java.lang.String) +Method isEnableStreamRequestType() : public throws (boolean) +Method isPostSubscriptionWhenPull() : public throws (boolean) +Method isUnitMode() : public throws (boolean) +Method isUseTLS() : public throws (boolean) +Method isVipChannelEnabled() : public throws (boolean) +Method maxOffset(org.apache.rocketmq.common.message.MessageQueue) : public throws (long) +Method minOffset(org.apache.rocketmq.common.message.MessageQueue) : public throws (long) +Method queryMessage(int,java.lang.String,java.lang.String,long,long) : public throws (org.apache.rocketmq.client.QueryResult) +Method queueWithNamespace(org.apache.rocketmq.common.message.MessageQueue) : public throws (org.apache.rocketmq.common.message.MessageQueue) +Method queuesWithNamespace(java.util.Collection) : public throws (java.util.Collection) +Method registerMessageListener(org.apache.rocketmq.client.consumer.listener.MessageListener) : public throws (void) +Method registerMessageListener(org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently) : public throws (void) +Method registerMessageListener(org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly) : public throws (void) +Method resetClientConfig(org.apache.rocketmq.client.ClientConfig) : public throws (void) +Method resume() : public throws (void) +Method searchOffset(long,org.apache.rocketmq.common.message.MessageQueue) : public throws (long) +Method sendMessageBack(int,java.lang.String,org.apache.rocketmq.common.message.MessageExt) : public throws (void) +Method sendMessageBack(int,org.apache.rocketmq.common.message.MessageExt) : public throws (void) +Method setAccessChannel(org.apache.rocketmq.client.AccessChannel) : public throws (void) +Method setAdjustThreadPoolNumsThreshold(long) : public throws (void) +Method setAllocateMessageQueueStrategy(org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy) : public throws (void) +Method setAwaitTerminationMillisWhenShutdown(long) : public throws (void) +Method setClientCallbackExecutorThreads(int) : public throws (void) +Method setClientIP(java.lang.String) : public throws (void) +Method setConsumeConcurrentlyMaxSpan(int) : public throws (void) +Method setConsumeFromWhere(org.apache.rocketmq.common.consumer.ConsumeFromWhere) : public throws (void) +Method setConsumeMessageBatchMaxSize(int) : public throws (void) +Method setConsumeThreadMax(int) : public throws (void) +Method setConsumeThreadMin(int) : public throws (void) +Method setConsumeTimeout(long) : public throws (void) +Method setConsumeTimestamp(java.lang.String) : public throws (void) +Method setConsumerGroup(java.lang.String) : public throws (void) +Method setEnableStreamRequestType(boolean) : public throws (void) +Method setHeartbeatBrokerInterval(int) : public throws (void) +Method setInstanceName(java.lang.String) : public throws (void) +Method setLanguage(org.apache.rocketmq.remoting.protocol.LanguageCode) : public throws (void) +Method setMaxReconsumeTimes(int) : public throws (void) +Method setMessageListener(org.apache.rocketmq.client.consumer.listener.MessageListener) : public throws (void) +Method setMessageModel(org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel) : public throws (void) +Method setMqClientApiTimeout(int) : public throws (void) +Method setNamespace(java.lang.String) : public throws (void) +Method setNamesrvAddr(java.lang.String) : public throws (void) +Method setOffsetStore(org.apache.rocketmq.client.consumer.store.OffsetStore) : public throws (void) +Method setPersistConsumerOffsetInterval(int) : public throws (void) +Method setPollNameServerInterval(int) : public throws (void) +Method setPostSubscriptionWhenPull(boolean) : public throws (void) +Method setPullBatchSize(int) : public throws (void) +Method setPullInterval(long) : public throws (void) +Method setPullThresholdForQueue(int) : public throws (void) +Method setPullThresholdForTopic(int) : public throws (void) +Method setPullThresholdSizeForQueue(int) : public throws (void) +Method setPullThresholdSizeForTopic(int) : public throws (void) +Method setPullTimeDelayMillsWhenException(long) : public throws (void) +Method setSubscription(java.util.Map) : public throws (void) +Method setSuspendCurrentQueueTimeMillis(long) : public throws (void) +Method setUnitMode(boolean) : public throws (void) +Method setUnitName(java.lang.String) : public throws (void) +Method setUseTLS(boolean) : public throws (void) +Method setVipChannelEnabled(boolean) : public throws (void) +Method shutdown() : public throws (void) +Method start() : public throws (void) +Method subscribe(java.lang.String,java.lang.String) : public throws (void) +Method subscribe(java.lang.String,java.lang.String,java.lang.String) : public throws (void) +Method subscribe(java.lang.String,org.apache.rocketmq.client.consumer.MessageSelector) : public throws (void) +Method suspend() : public throws (void) +Method toString() : public throws (java.lang.String) +Method unsubscribe(java.lang.String) : public throws (void) +Method updateCorePoolSize(int) : public throws (void) +Method viewMessage(java.lang.String) : public throws (org.apache.rocketmq.common.message.MessageExt) +Method viewMessage(java.lang.String,java.lang.String) : public throws (org.apache.rocketmq.common.message.MessageExt) +Method withNamespace(java.lang.String) : public throws (java.lang.String) +Method withNamespace(java.util.Set) : public throws (java.util.Set) +Method withoutNamespace(java.lang.String) : public throws (java.lang.String) +Method withoutNamespace(java.util.Set) : public throws (java.util.Set) diff --git a/test/src/test/resources/schema/api/client.consumer.PullCallback.schema b/test/src/test/resources/schema/api/client.consumer.PullCallback.schema new file mode 100644 index 0000000..8a01e2a --- /dev/null +++ b/test/src/test/resources/schema/api/client.consumer.PullCallback.schema @@ -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. + */ + + +Method onException(java.lang.Throwable) : public throws (void) +Method onSuccess(org.apache.rocketmq.client.consumer.PullResult) : public throws (void) diff --git a/test/src/test/resources/schema/api/client.consumer.PullResult.schema b/test/src/test/resources/schema/api/client.consumer.PullResult.schema new file mode 100644 index 0000000..1a7d877 --- /dev/null +++ b/test/src/test/resources/schema/api/client.consumer.PullResult.schema @@ -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. + */ + + +Field maxOffset : private long 0 +Field minOffset : private long 0 +Field msgFoundList : private java.util.List null +Field nextBeginOffset : private long 0 +Field pullStatus : private org.apache.rocketmq.client.consumer.PullStatus FOUND +Method getMaxOffset() : public throws (long) +Method getMinOffset() : public throws (long) +Method getMsgFoundList() : public throws (java.util.List) +Method getNextBeginOffset() : public throws (long) +Method getPullStatus() : public throws (org.apache.rocketmq.client.consumer.PullStatus) +Method setMsgFoundList(java.util.List) : public throws (void) +Method toString() : public throws (java.lang.String) diff --git a/test/src/test/resources/schema/api/client.consumer.PullStatus.schema b/test/src/test/resources/schema/api/client.consumer.PullStatus.schema new file mode 100644 index 0000000..97ebe5d --- /dev/null +++ b/test/src/test/resources/schema/api/client.consumer.PullStatus.schema @@ -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. + */ + + +Field FOUND : public int 0 +Field NO_MATCHED_MSG : public int 2 +Field NO_NEW_MSG : public int 1 +Field OFFSET_ILLEGAL : public int 3 diff --git a/test/src/test/resources/schema/api/client.consumer.listener.ConsumeConcurrentlyContext.schema b/test/src/test/resources/schema/api/client.consumer.listener.ConsumeConcurrentlyContext.schema new file mode 100644 index 0000000..a44f693 --- /dev/null +++ b/test/src/test/resources/schema/api/client.consumer.listener.ConsumeConcurrentlyContext.schema @@ -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. + */ + + +Field ackIndex : private int 2147483647 +Field delayLevelWhenNextConsume : private int 0 +Field messageQueue : private org.apache.rocketmq.common.message.MessageQueue null +Method getAckIndex() : public throws (int) +Method getDelayLevelWhenNextConsume() : public throws (int) +Method getMessageQueue() : public throws (org.apache.rocketmq.common.message.MessageQueue) +Method setAckIndex(int) : public throws (void) +Method setDelayLevelWhenNextConsume(int) : public throws (void) diff --git a/test/src/test/resources/schema/api/client.consumer.listener.ConsumeConcurrentlyStatus.schema b/test/src/test/resources/schema/api/client.consumer.listener.ConsumeConcurrentlyStatus.schema new file mode 100644 index 0000000..8af26e5 --- /dev/null +++ b/test/src/test/resources/schema/api/client.consumer.listener.ConsumeConcurrentlyStatus.schema @@ -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. + */ + + +Field CONSUME_SUCCESS : public int 0 +Field RECONSUME_LATER : public int 1 diff --git a/test/src/test/resources/schema/api/client.consumer.listener.ConsumeOrderlyContext.schema b/test/src/test/resources/schema/api/client.consumer.listener.ConsumeOrderlyContext.schema new file mode 100644 index 0000000..1586d50 --- /dev/null +++ b/test/src/test/resources/schema/api/client.consumer.listener.ConsumeOrderlyContext.schema @@ -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. + */ + + +Field autoCommit : private boolean true +Field messageQueue : private org.apache.rocketmq.common.message.MessageQueue null +Field suspendCurrentQueueTimeMillis : private long -1 +Method getMessageQueue() : public throws (org.apache.rocketmq.common.message.MessageQueue) +Method getSuspendCurrentQueueTimeMillis() : public throws (long) +Method isAutoCommit() : public throws (boolean) +Method setAutoCommit(boolean) : public throws (void) +Method setSuspendCurrentQueueTimeMillis(long) : public throws (void) diff --git a/test/src/test/resources/schema/api/client.consumer.listener.ConsumeOrderlyStatus.schema b/test/src/test/resources/schema/api/client.consumer.listener.ConsumeOrderlyStatus.schema new file mode 100644 index 0000000..6442348 --- /dev/null +++ b/test/src/test/resources/schema/api/client.consumer.listener.ConsumeOrderlyStatus.schema @@ -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. + */ + + +Field COMMIT : public int 2 +Field ROLLBACK : public int 1 +Field SUCCESS : public int 0 +Field SUSPEND_CURRENT_QUEUE_A_MOMENT : public int 3 diff --git a/test/src/test/resources/schema/api/client.consumer.listener.MessageListener.schema b/test/src/test/resources/schema/api/client.consumer.listener.MessageListener.schema new file mode 100644 index 0000000..a23f801 --- /dev/null +++ b/test/src/test/resources/schema/api/client.consumer.listener.MessageListener.schema @@ -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. + */ + + diff --git a/test/src/test/resources/schema/api/client.consumer.listener.MessageListenerConcurrently.schema b/test/src/test/resources/schema/api/client.consumer.listener.MessageListenerConcurrently.schema new file mode 100644 index 0000000..ea3c040 --- /dev/null +++ b/test/src/test/resources/schema/api/client.consumer.listener.MessageListenerConcurrently.schema @@ -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. + */ + + +Method consumeMessage(java.util.List,org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext) : public throws (org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus) diff --git a/test/src/test/resources/schema/api/client.consumer.listener.MessageListenerOrderly.schema b/test/src/test/resources/schema/api/client.consumer.listener.MessageListenerOrderly.schema new file mode 100644 index 0000000..05669b1 --- /dev/null +++ b/test/src/test/resources/schema/api/client.consumer.listener.MessageListenerOrderly.schema @@ -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. + */ + + +Method consumeMessage(java.util.List,org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext) : public throws (org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus) diff --git a/test/src/test/resources/schema/api/client.hook.CheckForbiddenHook.schema b/test/src/test/resources/schema/api/client.hook.CheckForbiddenHook.schema new file mode 100644 index 0000000..495b320 --- /dev/null +++ b/test/src/test/resources/schema/api/client.hook.CheckForbiddenHook.schema @@ -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. + */ + + +Method checkForbidden(org.apache.rocketmq.client.hook.CheckForbiddenContext) : public throws (void) +Method hookName() : public throws (java.lang.String) diff --git a/test/src/test/resources/schema/api/client.hook.ConsumeMessageContext.schema b/test/src/test/resources/schema/api/client.hook.ConsumeMessageContext.schema new file mode 100644 index 0000000..b09319a --- /dev/null +++ b/test/src/test/resources/schema/api/client.hook.ConsumeMessageContext.schema @@ -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. + */ + + +Field consumerGroup : private java.lang.String null +Field mq : private org.apache.rocketmq.common.message.MessageQueue null +Field mqTraceContext : private java.lang.Object null +Field msgList : private java.util.List null +Field namespace : private java.lang.String null +Field props : private java.util.Map null +Field status : private java.lang.String null +Field success : private boolean false +Method getConsumerGroup() : public throws (java.lang.String) +Method getMq() : public throws (org.apache.rocketmq.common.message.MessageQueue) +Method getMqTraceContext() : public throws (java.lang.Object) +Method getMsgList() : public throws (java.util.List) +Method getNamespace() : public throws (java.lang.String) +Method getProps() : public throws (java.util.Map) +Method getStatus() : public throws (java.lang.String) +Method isSuccess() : public throws (boolean) +Method setConsumerGroup(java.lang.String) : public throws (void) +Method setMq(org.apache.rocketmq.common.message.MessageQueue) : public throws (void) +Method setMqTraceContext(java.lang.Object) : public throws (void) +Method setMsgList(java.util.List) : public throws (void) +Method setNamespace(java.lang.String) : public throws (void) +Method setProps(java.util.Map) : public throws (void) +Method setStatus(java.lang.String) : public throws (void) +Method setSuccess(boolean) : public throws (void) diff --git a/test/src/test/resources/schema/api/client.hook.ConsumeMessageHook.schema b/test/src/test/resources/schema/api/client.hook.ConsumeMessageHook.schema new file mode 100644 index 0000000..b6a5951 --- /dev/null +++ b/test/src/test/resources/schema/api/client.hook.ConsumeMessageHook.schema @@ -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. + */ + + +Method consumeMessageAfter(org.apache.rocketmq.client.hook.ConsumeMessageContext) : public throws (void) +Method consumeMessageBefore(org.apache.rocketmq.client.hook.ConsumeMessageContext) : public throws (void) +Method hookName() : public throws (java.lang.String) diff --git a/test/src/test/resources/schema/api/client.hook.EndTransactionContext.schema b/test/src/test/resources/schema/api/client.hook.EndTransactionContext.schema new file mode 100644 index 0000000..5e0ecba --- /dev/null +++ b/test/src/test/resources/schema/api/client.hook.EndTransactionContext.schema @@ -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. + */ + + +Field brokerAddr : private java.lang.String null +Field fromTransactionCheck : private boolean false +Field message : private org.apache.rocketmq.common.message.Message null +Field msgId : private java.lang.String null +Field producerGroup : private java.lang.String null +Field transactionId : private java.lang.String null +Field transactionState : private org.apache.rocketmq.client.producer.LocalTransactionState null +Method getBrokerAddr() : public throws (java.lang.String) +Method getMessage() : public throws (org.apache.rocketmq.common.message.Message) +Method getMsgId() : public throws (java.lang.String) +Method getProducerGroup() : public throws (java.lang.String) +Method getTransactionId() : public throws (java.lang.String) +Method getTransactionState() : public throws (org.apache.rocketmq.client.producer.LocalTransactionState) +Method isFromTransactionCheck() : public throws (boolean) +Method setBrokerAddr(java.lang.String) : public throws (void) +Method setFromTransactionCheck(boolean) : public throws (void) +Method setMessage(org.apache.rocketmq.common.message.Message) : public throws (void) +Method setMsgId(java.lang.String) : public throws (void) +Method setProducerGroup(java.lang.String) : public throws (void) +Method setTransactionId(java.lang.String) : public throws (void) +Method setTransactionState(org.apache.rocketmq.client.producer.LocalTransactionState) : public throws (void) diff --git a/test/src/test/resources/schema/api/client.hook.EndTransactionHook.schema b/test/src/test/resources/schema/api/client.hook.EndTransactionHook.schema new file mode 100644 index 0000000..7aa94c0 --- /dev/null +++ b/test/src/test/resources/schema/api/client.hook.EndTransactionHook.schema @@ -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. + */ + + +Method endTransaction(org.apache.rocketmq.client.hook.EndTransactionContext) : public throws (void) +Method hookName() : public throws (java.lang.String) diff --git a/test/src/test/resources/schema/api/client.hook.FilterMessageContext.schema b/test/src/test/resources/schema/api/client.hook.FilterMessageContext.schema new file mode 100644 index 0000000..150eb67 --- /dev/null +++ b/test/src/test/resources/schema/api/client.hook.FilterMessageContext.schema @@ -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. + */ + + +Field arg : private java.lang.Object null +Field consumerGroup : private java.lang.String null +Field mq : private org.apache.rocketmq.common.message.MessageQueue null +Field msgList : private java.util.List null +Field unitMode : private boolean false +Method getArg() : public throws (java.lang.Object) +Method getConsumerGroup() : public throws (java.lang.String) +Method getMq() : public throws (org.apache.rocketmq.common.message.MessageQueue) +Method getMsgList() : public throws (java.util.List) +Method isUnitMode() : public throws (boolean) +Method setArg(java.lang.Object) : public throws (void) +Method setConsumerGroup(java.lang.String) : public throws (void) +Method setMq(org.apache.rocketmq.common.message.MessageQueue) : public throws (void) +Method setMsgList(java.util.List) : public throws (void) +Method setUnitMode(boolean) : public throws (void) +Method toString() : public throws (java.lang.String) diff --git a/test/src/test/resources/schema/api/client.hook.FilterMessageHook.schema b/test/src/test/resources/schema/api/client.hook.FilterMessageHook.schema new file mode 100644 index 0000000..9c28757 --- /dev/null +++ b/test/src/test/resources/schema/api/client.hook.FilterMessageHook.schema @@ -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. + */ + + +Method filterMessage(org.apache.rocketmq.client.hook.FilterMessageContext) : public throws (void) +Method hookName() : public throws (java.lang.String) diff --git a/test/src/test/resources/schema/api/client.hook.SendMessageContext.schema b/test/src/test/resources/schema/api/client.hook.SendMessageContext.schema new file mode 100644 index 0000000..529178f --- /dev/null +++ b/test/src/test/resources/schema/api/client.hook.SendMessageContext.schema @@ -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. + */ + + +Field bornHost : private java.lang.String null +Field brokerAddr : private java.lang.String null +Field communicationMode : private org.apache.rocketmq.client.impl.CommunicationMode null +Field exception : private java.lang.Exception null +Field message : private org.apache.rocketmq.common.message.Message null +Field mq : private org.apache.rocketmq.common.message.MessageQueue null +Field mqTraceContext : private java.lang.Object null +Field msgType : private org.apache.rocketmq.common.message.MessageType Normal_Msg +Field namespace : private java.lang.String null +Field producer : private org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl null +Field producerGroup : private java.lang.String null +Field props : private java.util.Map null +Field sendResult : private org.apache.rocketmq.client.producer.SendResult null +Method getBornHost() : public throws (java.lang.String) +Method getBrokerAddr() : public throws (java.lang.String) +Method getCommunicationMode() : public throws (org.apache.rocketmq.client.impl.CommunicationMode) +Method getException() : public throws (java.lang.Exception) +Method getMessage() : public throws (org.apache.rocketmq.common.message.Message) +Method getMq() : public throws (org.apache.rocketmq.common.message.MessageQueue) +Method getMqTraceContext() : public throws (java.lang.Object) +Method getMsgType() : public throws (org.apache.rocketmq.common.message.MessageType) +Method getNamespace() : public throws (java.lang.String) +Method getProducer() : public throws (org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl) +Method getProducerGroup() : public throws (java.lang.String) +Method getProps() : public throws (java.util.Map) +Method getSendResult() : public throws (org.apache.rocketmq.client.producer.SendResult) +Method setBornHost(java.lang.String) : public throws (void) +Method setBrokerAddr(java.lang.String) : public throws (void) +Method setCommunicationMode(org.apache.rocketmq.client.impl.CommunicationMode) : public throws (void) +Method setException(java.lang.Exception) : public throws (void) +Method setMessage(org.apache.rocketmq.common.message.Message) : public throws (void) +Method setMq(org.apache.rocketmq.common.message.MessageQueue) : public throws (void) +Method setMqTraceContext(java.lang.Object) : public throws (void) +Method setMsgType(org.apache.rocketmq.common.message.MessageType) : public throws (void) +Method setNamespace(java.lang.String) : public throws (void) +Method setProducer(org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl) : public throws (void) +Method setProducerGroup(java.lang.String) : public throws (void) +Method setProps(java.util.Map) : public throws (void) +Method setSendResult(org.apache.rocketmq.client.producer.SendResult) : public throws (void) diff --git a/test/src/test/resources/schema/api/client.hook.SendMessageHook.schema b/test/src/test/resources/schema/api/client.hook.SendMessageHook.schema new file mode 100644 index 0000000..ce673af --- /dev/null +++ b/test/src/test/resources/schema/api/client.hook.SendMessageHook.schema @@ -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. + */ + + +Method hookName() : public throws (java.lang.String) +Method sendMessageAfter(org.apache.rocketmq.client.hook.SendMessageContext) : public throws (void) +Method sendMessageBefore(org.apache.rocketmq.client.hook.SendMessageContext) : public throws (void) diff --git a/test/src/test/resources/schema/api/client.producer.DefaultMQProducer.schema b/test/src/test/resources/schema/api/client.producer.DefaultMQProducer.schema new file mode 100644 index 0000000..d1111fb --- /dev/null +++ b/test/src/test/resources/schema/api/client.producer.DefaultMQProducer.schema @@ -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. + */ + + +Field SEND_MESSAGE_WITH_VIP_CHANNEL_PROPERTY : public java.lang.String com.rocketmq.sendMessageWithVIPChannel +Field accessChannel : protected org.apache.rocketmq.client.AccessChannel LOCAL +Field clientCallbackExecutorThreads : private int null +Field clientIP : private java.lang.String null +Field compressMsgBodyOverHowmuch : private int 4096 +Field createTopicKey : private java.lang.String TBW102 +Field defaultMQProducerImpl : protected org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl null +Field defaultTopicQueueNums : private int 4 +Field enableStreamRequestType : protected boolean false +Field heartbeatBrokerInterval : private int 30000 +Field instanceName : private java.lang.String DEFAULT +Field language : private org.apache.rocketmq.remoting.protocol.LanguageCode JAVA +Field log : private org.apache.rocketmq.logging.InternalLogger null +Field maxMessageSize : private int 4194304 +Field mqClientApiTimeout : private int 3000 +Field namespace : protected java.lang.String null +Field namespaceInitialized : private boolean false +Field namesrvAddr : private java.lang.String null +Field persistConsumerOffsetInterval : private int 5000 +Field pollNameServerInterval : private int 30000 +Field producerGroup : private java.lang.String DEFAULT_PRODUCER +Field pullTimeDelayMillsWhenException : private long 1000 +Field retryAnotherBrokerWhenNotStoreOK : private boolean false +Field retryResponseCodes : private java.util.Set null +Field retryTimesWhenSendAsyncFailed : private int 2 +Field retryTimesWhenSendFailed : private int 2 +Field sendMsgTimeout : private int 3000 +Field traceDispatcher : private org.apache.rocketmq.client.trace.TraceDispatcher null +Field unitMode : private boolean false +Field unitName : private java.lang.String null +Field useTLS : private boolean false +Field vipChannelEnabled : private boolean false +Method addRetryResponseCode(int) : public throws (void) +Method buildMQClientId() : public throws (java.lang.String) +Method changeInstanceNameToPID() : public throws (void) +Method cloneClientConfig() : public throws (org.apache.rocketmq.client.ClientConfig) +Method createTopic(int,int,java.lang.String,java.lang.String) : public throws (void) +Method createTopic(int,java.lang.String,java.lang.String) : public throws (void) +Method earliestMsgStoreTime(org.apache.rocketmq.common.message.MessageQueue) : public throws (long) +Method fetchPublishMessageQueues(java.lang.String) : public throws (java.util.List) +Method getAccessChannel() : public throws (org.apache.rocketmq.client.AccessChannel) +Method getClientCallbackExecutorThreads() : public throws (int) +Method getClientIP() : public throws (java.lang.String) +Method getCompressMsgBodyOverHowmuch() : public throws (int) +Method getCreateTopicKey() : public throws (java.lang.String) +Method getDefaultMQProducerImpl() : public throws (org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl) +Method getDefaultTopicQueueNums() : public throws (int) +Method getHeartbeatBrokerInterval() : public throws (int) +Method getInstanceName() : public throws (java.lang.String) +Method getLanguage() : public throws (org.apache.rocketmq.remoting.protocol.LanguageCode) +Method getLatencyMax() : public throws ([J) +Method getMaxMessageSize() : public throws (int) +Method getMqClientApiTimeout() : public throws (int) +Method getNamespace() : public throws (java.lang.String) +Method getNamesrvAddr() : public throws (java.lang.String) +Method getNotAvailableDuration() : public throws ([J) +Method getPersistConsumerOffsetInterval() : public throws (int) +Method getPollNameServerInterval() : public throws (int) +Method getProducerGroup() : public throws (java.lang.String) +Method getPullTimeDelayMillsWhenException() : public throws (long) +Method getRetryResponseCodes() : public throws (java.util.Set) +Method getRetryTimesWhenSendAsyncFailed() : public throws (int) +Method getRetryTimesWhenSendFailed() : public throws (int) +Method getSendMsgTimeout() : public throws (int) +Method getTraceDispatcher() : public throws (org.apache.rocketmq.client.trace.TraceDispatcher) +Method getUnitName() : public throws (java.lang.String) +Method isEnableStreamRequestType() : public throws (boolean) +Method isRetryAnotherBrokerWhenNotStoreOK() : public throws (boolean) +Method isSendLatencyFaultEnable() : public throws (boolean) +Method isSendMessageWithVIPChannel() : public throws (boolean) +Method isUnitMode() : public throws (boolean) +Method isUseTLS() : public throws (boolean) +Method isVipChannelEnabled() : public throws (boolean) +Method maxOffset(org.apache.rocketmq.common.message.MessageQueue) : public throws (long) +Method minOffset(org.apache.rocketmq.common.message.MessageQueue) : public throws (long) +Method queryMessage(int,java.lang.String,java.lang.String,long,long) : public throws (org.apache.rocketmq.client.QueryResult) +Method queueWithNamespace(org.apache.rocketmq.common.message.MessageQueue) : public throws (org.apache.rocketmq.common.message.MessageQueue) +Method queuesWithNamespace(java.util.Collection) : public throws (java.util.Collection) +Method request(java.lang.Object,long,org.apache.rocketmq.client.producer.MessageQueueSelector,org.apache.rocketmq.client.producer.RequestCallback,org.apache.rocketmq.common.message.Message) : public throws (void) +Method request(java.lang.Object,long,org.apache.rocketmq.client.producer.MessageQueueSelector,org.apache.rocketmq.common.message.Message) : public throws (org.apache.rocketmq.common.message.Message) +Method request(long,org.apache.rocketmq.client.producer.RequestCallback,org.apache.rocketmq.common.message.Message) : public throws (void) +Method request(long,org.apache.rocketmq.client.producer.RequestCallback,org.apache.rocketmq.common.message.Message,org.apache.rocketmq.common.message.MessageQueue) : public throws (void) +Method request(long,org.apache.rocketmq.common.message.Message) : public throws (org.apache.rocketmq.common.message.Message) +Method request(long,org.apache.rocketmq.common.message.Message,org.apache.rocketmq.common.message.MessageQueue) : public throws (org.apache.rocketmq.common.message.Message) +Method resetClientConfig(org.apache.rocketmq.client.ClientConfig) : public throws (void) +Method searchOffset(long,org.apache.rocketmq.common.message.MessageQueue) : public throws (long) +Method send(java.lang.Object,long,org.apache.rocketmq.client.producer.MessageQueueSelector,org.apache.rocketmq.client.producer.SendCallback,org.apache.rocketmq.common.message.Message) : public throws (void) +Method send(java.lang.Object,long,org.apache.rocketmq.client.producer.MessageQueueSelector,org.apache.rocketmq.common.message.Message) : public throws (org.apache.rocketmq.client.producer.SendResult) +Method send(java.lang.Object,org.apache.rocketmq.client.producer.MessageQueueSelector,org.apache.rocketmq.client.producer.SendCallback,org.apache.rocketmq.common.message.Message) : public throws (void) +Method send(java.lang.Object,org.apache.rocketmq.client.producer.MessageQueueSelector,org.apache.rocketmq.common.message.Message) : public throws (org.apache.rocketmq.client.producer.SendResult) +Method send(java.util.Collection) : public throws (org.apache.rocketmq.client.producer.SendResult) +Method send(java.util.Collection,long) : public throws (org.apache.rocketmq.client.producer.SendResult) +Method send(java.util.Collection,long,org.apache.rocketmq.client.producer.SendCallback) : public throws (void) +Method send(java.util.Collection,long,org.apache.rocketmq.client.producer.SendCallback,org.apache.rocketmq.common.message.MessageQueue) : public throws (void) +Method send(java.util.Collection,long,org.apache.rocketmq.common.message.MessageQueue) : public throws (org.apache.rocketmq.client.producer.SendResult) +Method send(java.util.Collection,org.apache.rocketmq.client.producer.SendCallback) : public throws (void) +Method send(java.util.Collection,org.apache.rocketmq.client.producer.SendCallback,org.apache.rocketmq.common.message.MessageQueue) : public throws (void) +Method send(java.util.Collection,org.apache.rocketmq.common.message.MessageQueue) : public throws (org.apache.rocketmq.client.producer.SendResult) +Method send(long,org.apache.rocketmq.client.producer.SendCallback,org.apache.rocketmq.common.message.Message) : public throws (void) +Method send(long,org.apache.rocketmq.client.producer.SendCallback,org.apache.rocketmq.common.message.Message,org.apache.rocketmq.common.message.MessageQueue) : public throws (void) +Method send(long,org.apache.rocketmq.common.message.Message) : public throws (org.apache.rocketmq.client.producer.SendResult) +Method send(long,org.apache.rocketmq.common.message.Message,org.apache.rocketmq.common.message.MessageQueue) : public throws (org.apache.rocketmq.client.producer.SendResult) +Method send(org.apache.rocketmq.client.producer.SendCallback,org.apache.rocketmq.common.message.Message) : public throws (void) +Method send(org.apache.rocketmq.client.producer.SendCallback,org.apache.rocketmq.common.message.Message,org.apache.rocketmq.common.message.MessageQueue) : public throws (void) +Method send(org.apache.rocketmq.common.message.Message) : public throws (org.apache.rocketmq.client.producer.SendResult) +Method send(org.apache.rocketmq.common.message.Message,org.apache.rocketmq.common.message.MessageQueue) : public throws (org.apache.rocketmq.client.producer.SendResult) +Method sendMessageInTransaction(java.lang.Object,org.apache.rocketmq.common.message.Message) : public throws (org.apache.rocketmq.client.producer.TransactionSendResult) +Method sendOneway(java.lang.Object,org.apache.rocketmq.client.producer.MessageQueueSelector,org.apache.rocketmq.common.message.Message) : public throws (void) +Method sendOneway(org.apache.rocketmq.common.message.Message) : public throws (void) +Method sendOneway(org.apache.rocketmq.common.message.Message,org.apache.rocketmq.common.message.MessageQueue) : public throws (void) +Method setAccessChannel(org.apache.rocketmq.client.AccessChannel) : public throws (void) +Method setAsyncSenderExecutor(java.util.concurrent.ExecutorService) : public throws (void) +Method setCallbackExecutor(java.util.concurrent.ExecutorService) : public throws (void) +Method setClientCallbackExecutorThreads(int) : public throws (void) +Method setClientIP(java.lang.String) : public throws (void) +Method setCompressMsgBodyOverHowmuch(int) : public throws (void) +Method setCreateTopicKey(java.lang.String) : public throws (void) +Method setDefaultTopicQueueNums(int) : public throws (void) +Method setEnableStreamRequestType(boolean) : public throws (void) +Method setHeartbeatBrokerInterval(int) : public throws (void) +Method setInstanceName(java.lang.String) : public throws (void) +Method setLanguage(org.apache.rocketmq.remoting.protocol.LanguageCode) : public throws (void) +Method setLatencyMax([J) : public throws (void) +Method setMaxMessageSize(int) : public throws (void) +Method setMqClientApiTimeout(int) : public throws (void) +Method setNamespace(java.lang.String) : public throws (void) +Method setNamesrvAddr(java.lang.String) : public throws (void) +Method setNotAvailableDuration([J) : public throws (void) +Method setPersistConsumerOffsetInterval(int) : public throws (void) +Method setPollNameServerInterval(int) : public throws (void) +Method setProducerGroup(java.lang.String) : public throws (void) +Method setPullTimeDelayMillsWhenException(long) : public throws (void) +Method setRetryAnotherBrokerWhenNotStoreOK(boolean) : public throws (void) +Method setRetryTimesWhenSendAsyncFailed(int) : public throws (void) +Method setRetryTimesWhenSendFailed(int) : public throws (void) +Method setSendLatencyFaultEnable(boolean) : public throws (void) +Method setSendMessageWithVIPChannel(boolean) : public throws (void) +Method setSendMsgTimeout(int) : public throws (void) +Method setUnitMode(boolean) : public throws (void) +Method setUnitName(java.lang.String) : public throws (void) +Method setUseTLS(boolean) : public throws (void) +Method setVipChannelEnabled(boolean) : public throws (void) +Method shutdown() : public throws (void) +Method start() : public throws (void) +Method toString() : public throws (java.lang.String) +Method viewMessage(java.lang.String) : public throws (org.apache.rocketmq.common.message.MessageExt) +Method viewMessage(java.lang.String,java.lang.String) : public throws (org.apache.rocketmq.common.message.MessageExt) +Method withNamespace(java.lang.String) : public throws (java.lang.String) +Method withNamespace(java.util.Set) : public throws (java.util.Set) +Method withoutNamespace(java.lang.String) : public throws (java.lang.String) +Method withoutNamespace(java.util.Set) : public throws (java.util.Set) diff --git a/test/src/test/resources/schema/api/client.producer.MessageQueueSelector.schema b/test/src/test/resources/schema/api/client.producer.MessageQueueSelector.schema new file mode 100644 index 0000000..a457e1d --- /dev/null +++ b/test/src/test/resources/schema/api/client.producer.MessageQueueSelector.schema @@ -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. + */ + + +Method select(java.lang.Object,java.util.List,org.apache.rocketmq.common.message.Message) : public throws (org.apache.rocketmq.common.message.MessageQueue) diff --git a/test/src/test/resources/schema/api/client.producer.SendCallback.schema b/test/src/test/resources/schema/api/client.producer.SendCallback.schema new file mode 100644 index 0000000..106228a --- /dev/null +++ b/test/src/test/resources/schema/api/client.producer.SendCallback.schema @@ -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. + */ + + +Method onException(java.lang.Throwable) : public throws (void) +Method onSuccess(org.apache.rocketmq.client.producer.SendResult) : public throws (void) diff --git a/test/src/test/resources/schema/api/client.producer.SendResult.schema b/test/src/test/resources/schema/api/client.producer.SendResult.schema new file mode 100644 index 0000000..ed38eb1 --- /dev/null +++ b/test/src/test/resources/schema/api/client.producer.SendResult.schema @@ -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. + */ + + +Field messageQueue : private org.apache.rocketmq.common.message.MessageQueue null +Field msgId : private java.lang.String null +Field offsetMsgId : private java.lang.String null +Field queueOffset : private long 0 +Field regionId : private java.lang.String null +Field sendStatus : private org.apache.rocketmq.client.producer.SendStatus null +Field traceOn : private boolean true +Field transactionId : private java.lang.String null +Method decoderSendResultFromJson(java.lang.String) : public throws (org.apache.rocketmq.client.producer.SendResult) +Method encoderSendResultToJson(java.lang.Object) : public throws (java.lang.String) +Method getMessageQueue() : public throws (org.apache.rocketmq.common.message.MessageQueue) +Method getMsgId() : public throws (java.lang.String) +Method getOffsetMsgId() : public throws (java.lang.String) +Method getQueueOffset() : public throws (long) +Method getRegionId() : public throws (java.lang.String) +Method getSendStatus() : public throws (org.apache.rocketmq.client.producer.SendStatus) +Method getTransactionId() : public throws (java.lang.String) +Method isTraceOn() : public throws (boolean) +Method setMessageQueue(org.apache.rocketmq.common.message.MessageQueue) : public throws (void) +Method setMsgId(java.lang.String) : public throws (void) +Method setOffsetMsgId(java.lang.String) : public throws (void) +Method setQueueOffset(long) : public throws (void) +Method setRegionId(java.lang.String) : public throws (void) +Method setSendStatus(org.apache.rocketmq.client.producer.SendStatus) : public throws (void) +Method setTraceOn(boolean) : public throws (void) +Method setTransactionId(java.lang.String) : public throws (void) +Method toString() : public throws (java.lang.String) diff --git a/test/src/test/resources/schema/api/client.producer.SendStatus.schema b/test/src/test/resources/schema/api/client.producer.SendStatus.schema new file mode 100644 index 0000000..32edfb9 --- /dev/null +++ b/test/src/test/resources/schema/api/client.producer.SendStatus.schema @@ -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. + */ + + +Field FLUSH_DISK_TIMEOUT : public int 1 +Field FLUSH_SLAVE_TIMEOUT : public int 2 +Field SEND_OK : public int 0 +Field SLAVE_NOT_AVAILABLE : public int 3 diff --git a/test/src/test/resources/schema/api/common.message.Message.schema b/test/src/test/resources/schema/api/common.message.Message.schema new file mode 100644 index 0000000..95f4d41 --- /dev/null +++ b/test/src/test/resources/schema/api/common.message.Message.schema @@ -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. + */ + + +Field body : private [B null +Field flag : private int 0 +Field properties : private java.util.Map null +Field serialVersionUID : private long 8445773977080406428 +Field topic : private java.lang.String null +Field transactionId : private java.lang.String null +Method getBody() : public throws ([B) +Method getBuyerId() : public throws (java.lang.String) +Method getDelayTimeLevel() : public throws (int) +Method getFlag() : public throws (int) +Method getKeys() : public throws (java.lang.String) +Method getProperties() : public throws (java.util.Map) +Method getProperty(java.lang.String) : public throws (java.lang.String) +Method getTags() : public throws (java.lang.String) +Method getTopic() : public throws (java.lang.String) +Method getTransactionId() : public throws (java.lang.String) +Method getUserProperty(java.lang.String) : public throws (java.lang.String) +Method isWaitStoreMsgOK() : public throws (boolean) +Method putUserProperty(java.lang.String,java.lang.String) : public throws (void) +Method setBody([B) : public throws (void) +Method setBuyerId(java.lang.String) : public throws (void) +Method setDelayTimeLevel(int) : public throws (void) +Method setFlag(int) : public throws (void) +Method setInstanceId(java.lang.String) : public throws (void) +Method setKeys(java.lang.String) : public throws (void) +Method setKeys(java.util.Collection) : public throws (void) +Method setTags(java.lang.String) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) +Method setTransactionId(java.lang.String) : public throws (void) +Method setWaitStoreMsgOK(boolean) : public throws (void) +Method toString() : public throws (java.lang.String) diff --git a/test/src/test/resources/schema/api/common.message.MessageExt.schema b/test/src/test/resources/schema/api/common.message.MessageExt.schema new file mode 100644 index 0000000..72bd52e --- /dev/null +++ b/test/src/test/resources/schema/api/common.message.MessageExt.schema @@ -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. + */ + + +Field body : private [B null +Field bodyCRC : private int 0 +Field bornHost : private java.net.SocketAddress null +Field bornTimestamp : private long 0 +Field brokerName : private java.lang.String null +Field commitLogOffset : private long 0 +Field flag : private int 0 +Field msgId : private java.lang.String null +Field preparedTransactionOffset : private long 0 +Field properties : private java.util.Map null +Field queueId : private int 0 +Field queueOffset : private long 0 +Field reconsumeTimes : private int 0 +Field serialVersionUID : private long 8445773977080406428 +Field storeHost : private java.net.SocketAddress null +Field storeSize : private int 0 +Field storeTimestamp : private long 0 +Field sysFlag : private int 0 +Field topic : private java.lang.String null +Field transactionId : private java.lang.String null +Method getBody() : public throws ([B) +Method getBodyCRC() : public throws (int) +Method getBornHost() : public throws (java.net.SocketAddress) +Method getBornHostBytes() : public throws (java.nio.ByteBuffer) +Method getBornHostBytes(java.nio.ByteBuffer) : public throws (java.nio.ByteBuffer) +Method getBornHostNameString() : public throws (java.lang.String) +Method getBornHostString() : public throws (java.lang.String) +Method getBornTimestamp() : public throws (long) +Method getBrokerName() : public throws (java.lang.String) +Method getBuyerId() : public throws (java.lang.String) +Method getCommitLogOffset() : public throws (long) +Method getDelayTimeLevel() : public throws (int) +Method getFlag() : public throws (int) +Method getKeys() : public throws (java.lang.String) +Method getMsgId() : public throws (java.lang.String) +Method getPreparedTransactionOffset() : public throws (long) +Method getProperties() : public throws (java.util.Map) +Method getProperty(java.lang.String) : public throws (java.lang.String) +Method getQueueId() : public throws (int) +Method getQueueOffset() : public throws (long) +Method getReconsumeTimes() : public throws (int) +Method getStoreHost() : public throws (java.net.SocketAddress) +Method getStoreHostBytes() : public throws (java.nio.ByteBuffer) +Method getStoreHostBytes(java.nio.ByteBuffer) : public throws (java.nio.ByteBuffer) +Method getStoreSize() : public throws (int) +Method getStoreTimestamp() : public throws (long) +Method getSysFlag() : public throws (int) +Method getTags() : public throws (java.lang.String) +Method getTopic() : public throws (java.lang.String) +Method getTransactionId() : public throws (java.lang.String) +Method getUserProperty(java.lang.String) : public throws (java.lang.String) +Method isWaitStoreMsgOK() : public throws (boolean) +Method parseTopicFilterType(int) : public throws (org.apache.rocketmq.common.TopicFilterType) +Method putUserProperty(java.lang.String,java.lang.String) : public throws (void) +Method setBody([B) : public throws (void) +Method setBodyCRC(int) : public throws (void) +Method setBornHost(java.net.SocketAddress) : public throws (void) +Method setBornHostV6Flag() : public throws (void) +Method setBornTimestamp(long) : public throws (void) +Method setBrokerName(java.lang.String) : public throws (void) +Method setBuyerId(java.lang.String) : public throws (void) +Method setCommitLogOffset(long) : public throws (void) +Method setDelayTimeLevel(int) : public throws (void) +Method setFlag(int) : public throws (void) +Method setInstanceId(java.lang.String) : public throws (void) +Method setKeys(java.lang.String) : public throws (void) +Method setKeys(java.util.Collection) : public throws (void) +Method setMsgId(java.lang.String) : public throws (void) +Method setPreparedTransactionOffset(long) : public throws (void) +Method setQueueId(int) : public throws (void) +Method setQueueOffset(long) : public throws (void) +Method setReconsumeTimes(int) : public throws (void) +Method setStoreHost(java.net.SocketAddress) : public throws (void) +Method setStoreHostAddressV6Flag() : public throws (void) +Method setStoreSize(int) : public throws (void) +Method setStoreTimestamp(long) : public throws (void) +Method setSysFlag(int) : public throws (void) +Method setTags(java.lang.String) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) +Method setTransactionId(java.lang.String) : public throws (void) +Method setWaitStoreMsgOK(boolean) : public throws (void) +Method socketAddress2ByteBuffer(java.net.SocketAddress) : public throws (java.nio.ByteBuffer) +Method socketAddress2ByteBuffer(java.net.SocketAddress,java.nio.ByteBuffer) : public throws (java.nio.ByteBuffer) +Method toString() : public throws (java.lang.String) diff --git a/test/src/test/resources/schema/api/common.message.MessageQueue.schema b/test/src/test/resources/schema/api/common.message.MessageQueue.schema new file mode 100644 index 0000000..058e17e --- /dev/null +++ b/test/src/test/resources/schema/api/common.message.MessageQueue.schema @@ -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. + */ + + +Field brokerName : private java.lang.String null +Field queueId : private int 0 +Field serialVersionUID : private long 6191200464116433425 +Field topic : private java.lang.String null +Method compareTo(java.lang.Object) : public throws (int) +Method compareTo(org.apache.rocketmq.common.message.MessageQueue) : public throws (int) +Method equals(java.lang.Object) : public throws (boolean) +Method getBrokerName() : public throws (java.lang.String) +Method getQueueId() : public throws (int) +Method getTopic() : public throws (java.lang.String) +Method hashCode() : public throws (int) +Method setBrokerName(java.lang.String) : public throws (void) +Method setQueueId(int) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) +Method toString() : public throws (java.lang.String) diff --git a/test/src/test/resources/schema/api/remoting.RPCHook.schema b/test/src/test/resources/schema/api/remoting.RPCHook.schema new file mode 100644 index 0000000..28df1ab --- /dev/null +++ b/test/src/test/resources/schema/api/remoting.RPCHook.schema @@ -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. + */ + + +Method doAfterResponse(java.lang.String,org.apache.rocketmq.remoting.protocol.RemotingCommand,org.apache.rocketmq.remoting.protocol.RemotingCommand) : public throws (void) +Method doBeforeRequest(java.lang.String,org.apache.rocketmq.remoting.protocol.RemotingCommand) : public throws (void) diff --git a/test/src/test/resources/schema/api/tools.admin.DefaultMQAdminExt.schema b/test/src/test/resources/schema/api/tools.admin.DefaultMQAdminExt.schema new file mode 100644 index 0000000..026c197 --- /dev/null +++ b/test/src/test/resources/schema/api/tools.admin.DefaultMQAdminExt.schema @@ -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. + */ + + +Field SEND_MESSAGE_WITH_VIP_CHANNEL_PROPERTY : public java.lang.String com.rocketmq.sendMessageWithVIPChannel +Field accessChannel : protected org.apache.rocketmq.client.AccessChannel LOCAL +Field adminExtGroup : private java.lang.String admin_ext_group +Field clientCallbackExecutorThreads : private int null +Field clientIP : private java.lang.String null +Field createTopicKey : private java.lang.String TBW102 +Field defaultMQAdminExtImpl : private org.apache.rocketmq.tools.admin.DefaultMQAdminExtImpl null +Field enableStreamRequestType : protected boolean false +Field heartbeatBrokerInterval : private int 30000 +Field instanceName : private java.lang.String DEFAULT +Field language : private org.apache.rocketmq.remoting.protocol.LanguageCode JAVA +Field mqClientApiTimeout : private int 3000 +Field namespace : protected java.lang.String null +Field namespaceInitialized : private boolean false +Field namesrvAddr : private java.lang.String null +Field persistConsumerOffsetInterval : private int 5000 +Field pollNameServerInterval : private int 30000 +Field pullTimeDelayMillsWhenException : private long 1000 +Field timeoutMillis : private long 5000 +Field unitMode : private boolean false +Field unitName : private java.lang.String null +Field useTLS : private boolean false +Field vipChannelEnabled : private boolean false +Method addWritePermOfBroker(java.lang.String,java.lang.String) : public throws (int) +Method buildMQClientId() : public throws (java.lang.String) +Method changeInstanceNameToPID() : public throws (void) +Method cleanExpiredConsumerQueue(java.lang.String) : public throws (boolean) +Method cleanExpiredConsumerQueueByAddr(java.lang.String) : public throws (boolean) +Method cleanUnusedTopic(java.lang.String) : public throws (boolean) +Method cleanUnusedTopicByAddr(java.lang.String) : public throws (boolean) +Method cloneClientConfig() : public throws (org.apache.rocketmq.client.ClientConfig) +Method cloneGroupOffset(boolean,java.lang.String,java.lang.String,java.lang.String) : public throws (void) +Method consumeMessageDirectly(java.lang.String,java.lang.String,java.lang.String) : public throws (org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult) +Method consumeMessageDirectly(java.lang.String,java.lang.String,java.lang.String,java.lang.String) : public throws (org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult) +Method createAndUpdateKvConfig(java.lang.String,java.lang.String,java.lang.String) : public throws (void) +Method createAndUpdatePlainAccessConfig(java.lang.String,org.apache.rocketmq.auth.migration.plain.PlainAccessConfig) : public throws (void) +Method createAndUpdateSubscriptionGroupConfig(java.lang.String,org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig) : public throws (void) +Method createAndUpdateTopicConfig(java.lang.String,org.apache.rocketmq.common.TopicConfig) : public throws (void) +Method createOrUpdateOrderConf(boolean,java.lang.String,java.lang.String) : public throws (void) +Method createTopic(int,int,java.lang.String,java.lang.String) : public throws (void) +Method createTopic(int,java.lang.String,java.lang.String) : public throws (void) +Method deleteExpiredCommitLog(java.lang.String) : public throws (boolean) +Method deleteExpiredCommitLogByAddr(java.lang.String) : public throws (boolean) +Method deleteKvConfig(java.lang.String,java.lang.String) : public throws (void) +Method deletePlainAccessConfig(java.lang.String,java.lang.String) : public throws (void) +Method deleteSubscriptionGroup(boolean,java.lang.String,java.lang.String) : public throws (void) +Method deleteSubscriptionGroup(java.lang.String,java.lang.String) : public throws (void) +Method deleteTopicInBroker(java.lang.String,java.util.Set) : public throws (void) +Method deleteTopicInNameServer(java.lang.String,java.lang.String,java.util.Set) : public throws (void) +Method earliestMsgStoreTime(org.apache.rocketmq.common.message.MessageQueue) : public throws (long) +Method examineBrokerClusterAclConfig(java.lang.String) : public throws (org.apache.rocketmq.auth.migration.plain.AclConfig) +Method examineBrokerClusterAclVersionInfo(java.lang.String) : public throws (org.apache.rocketmq.remoting.protocol.body.ClusterAclVersionInfo) +Method examineBrokerClusterInfo() : public throws (org.apache.rocketmq.remoting.protocol.body.ClusterInfo) +Method examineConsumeStats(java.lang.String) : public throws (org.apache.rocketmq.remoting.protocol.admin.ConsumeStats) +Method examineConsumeStats(java.lang.String,java.lang.String) : public throws (org.apache.rocketmq.remoting.protocol.admin.ConsumeStats) +Method examineConsumerConnectionInfo(java.lang.String) : public throws (org.apache.rocketmq.remoting.protocol.body.ConsumerConnection) +Method examineConsumerConnectionInfo(java.lang.String,java.lang.String) : public throws (org.apache.rocketmq.remoting.protocol.body.ConsumerConnection) +Method examineProducerConnectionInfo(java.lang.String,java.lang.String) : public throws (org.apache.rocketmq.remoting.protocol.body.ProducerConnection) +Method examineSubscriptionGroupConfig(java.lang.String,java.lang.String) : public throws (org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig) +Method examineTopicConfig(java.lang.String,java.lang.String) : public throws (org.apache.rocketmq.common.TopicConfig) +Method examineTopicRouteInfo(java.lang.String) : public throws (org.apache.rocketmq.remoting.protocol.route.TopicRouteData) +Method examineTopicStats(java.lang.String) : public throws (org.apache.rocketmq.remoting.protocol.admin.TopicStatsTable) +Method fetchAllTopicList() : public throws (org.apache.rocketmq.remoting.protocol.body.TopicList) +Method fetchBrokerRuntimeStats(java.lang.String) : public throws (org.apache.rocketmq.remoting.protocol.body.KVTable) +Method fetchConsumeStatsInBroker(boolean,java.lang.String,long) : public throws (org.apache.rocketmq.remoting.protocol.body.ConsumeStatsList) +Method fetchTopicsByCLuster(java.lang.String) : public throws (org.apache.rocketmq.remoting.protocol.body.TopicList) +Method getAccessChannel() : public throws (org.apache.rocketmq.client.AccessChannel) +Method getAdminExtGroup() : public throws (java.lang.String) +Method getAllProducerInfo(java.lang.String) : public throws (org.apache.rocketmq.remoting.protocol.body.ProducerTableInfo) +Method getAllSubscriptionGroup(java.lang.String,long) : public throws (org.apache.rocketmq.remoting.protocol.body.SubscriptionGroupWrapper) +Method getAllTopicConfig(java.lang.String,long) : public throws (org.apache.rocketmq.remoting.protocol.body.TopicConfigSerializeWrapper) +Method getBrokerConfig(java.lang.String) : public throws (java.util.Properties) +Method getClientCallbackExecutorThreads() : public throws (int) +Method getClientIP() : public throws (java.lang.String) +Method getClusterList(java.lang.String) : public throws (java.util.Set) +Method getConsumeStatus(java.lang.String,java.lang.String,java.lang.String) : public throws (java.util.Map) +Method getConsumerRunningInfo(boolean,java.lang.String,java.lang.String) : public throws (org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo) +Method getCreateTopicKey() : public throws (java.lang.String) +Method getHeartbeatBrokerInterval() : public throws (int) +Method getInstanceName() : public throws (java.lang.String) +Method getKVConfig(java.lang.String,java.lang.String) : public throws (java.lang.String) +Method getKVListByNamespace(java.lang.String) : public throws (org.apache.rocketmq.remoting.protocol.body.KVTable) +Method getLanguage() : public throws (org.apache.rocketmq.remoting.protocol.LanguageCode) +Method getMqClientApiTimeout() : public throws (int) +Method getNameServerAddressList() : public throws (java.util.List) +Method getNameServerConfig(java.util.List) : public throws (java.util.Map) +Method getNamespace() : public throws (java.lang.String) +Method getNamesrvAddr() : public throws (java.lang.String) +Method getPersistConsumerOffsetInterval() : public throws (int) +Method getPollNameServerInterval() : public throws (int) +Method getPullTimeDelayMillsWhenException() : public throws (long) +Method getTopicClusterList(java.lang.String) : public throws (java.util.Set) +Method getUnitName() : public throws (java.lang.String) +Method getUserSubscriptionGroup(java.lang.String,long) : public throws (org.apache.rocketmq.remoting.protocol.body.SubscriptionGroupWrapper) +Method getUserTopicConfig(boolean,java.lang.String,long) : public throws (org.apache.rocketmq.remoting.protocol.body.TopicConfigSerializeWrapper) +Method isEnableStreamRequestType() : public throws (boolean) +Method isUnitMode() : public throws (boolean) +Method isUseTLS() : public throws (boolean) +Method isVipChannelEnabled() : public throws (boolean) +Method maxOffset(org.apache.rocketmq.common.message.MessageQueue) : public throws (long) +Method messageTrackDetail(org.apache.rocketmq.common.message.MessageExt) : public throws (java.util.List) +Method minOffset(org.apache.rocketmq.common.message.MessageQueue) : public throws (long) +Method putKVConfig(java.lang.String,java.lang.String,java.lang.String) : public throws (void) +Method queryConsumeQueue(int,int,java.lang.String,java.lang.String,java.lang.String,long) : public throws (org.apache.rocketmq.remoting.protocol.body.QueryConsumeQueueResponseBody) +Method queryConsumeTimeSpan(java.lang.String,java.lang.String) : public throws (java.util.List) +Method queryMessage(int,java.lang.String,java.lang.String,long,long) : public throws (org.apache.rocketmq.client.QueryResult) +Method queryMessageByUniqKey(int,java.lang.String,java.lang.String,long,long) : public throws (org.apache.rocketmq.client.QueryResult) +Method queryTopicConsumeByWho(java.lang.String) : public throws (org.apache.rocketmq.remoting.protocol.body.GroupList) +Method queueWithNamespace(org.apache.rocketmq.common.message.MessageQueue) : public throws (org.apache.rocketmq.common.message.MessageQueue) +Method queuesWithNamespace(java.util.Collection) : public throws (java.util.Collection) +Method resetClientConfig(org.apache.rocketmq.client.ClientConfig) : public throws (void) +Method resetOffsetByTimestamp(boolean,boolean,java.lang.String,java.lang.String,long) : public throws (java.util.Map) +Method resetOffsetByTimestamp(boolean,java.lang.String,java.lang.String,long) : public throws (java.util.Map) +Method resetOffsetByTimestampOld(boolean,java.lang.String,java.lang.String,long) : public throws (java.util.List) +Method resetOffsetNew(java.lang.String,java.lang.String,long) : public throws (void) +Method resumeCheckHalfMessage(java.lang.String) : public throws (boolean) +Method resumeCheckHalfMessage(java.lang.String,java.lang.String) : public throws (boolean) +Method searchOffset(long,org.apache.rocketmq.common.message.MessageQueue) : public throws (long) +Method setAccessChannel(org.apache.rocketmq.client.AccessChannel) : public throws (void) +Method setAdminExtGroup(java.lang.String) : public throws (void) +Method setClientCallbackExecutorThreads(int) : public throws (void) +Method setClientIP(java.lang.String) : public throws (void) +Method setCreateTopicKey(java.lang.String) : public throws (void) +Method setEnableStreamRequestType(boolean) : public throws (void) +Method setHeartbeatBrokerInterval(int) : public throws (void) +Method setInstanceName(java.lang.String) : public throws (void) +Method setLanguage(org.apache.rocketmq.remoting.protocol.LanguageCode) : public throws (void) +Method setMqClientApiTimeout(int) : public throws (void) +Method setNamespace(java.lang.String) : public throws (void) +Method setNamesrvAddr(java.lang.String) : public throws (void) +Method setPersistConsumerOffsetInterval(int) : public throws (void) +Method setPollNameServerInterval(int) : public throws (void) +Method setPullTimeDelayMillsWhenException(long) : public throws (void) +Method setUnitMode(boolean) : public throws (void) +Method setUnitName(java.lang.String) : public throws (void) +Method setUseTLS(boolean) : public throws (void) +Method setVipChannelEnabled(boolean) : public throws (void) +Method shutdown() : public throws (void) +Method start() : public throws (void) +Method toString() : public throws (java.lang.String) +Method updateBrokerConfig(java.lang.String,java.util.Properties) : public throws (void) +Method updateConsumeOffset(java.lang.String,java.lang.String,long,org.apache.rocketmq.common.message.MessageQueue) : public throws (void) +Method updateGlobalWhiteAddrConfig(java.lang.String,java.lang.String) : public throws (void) +Method updateGlobalWhiteAddrConfig(java.lang.String,java.lang.String,java.lang.String) : public throws (void) +Method updateNameServerConfig(java.util.List,java.util.Properties) : public throws (void) +Method viewBrokerStatsData(java.lang.String,java.lang.String,java.lang.String) : public throws (org.apache.rocketmq.remoting.protocol.body.BrokerStatsData) +Method viewMessage(java.lang.String) : public throws (org.apache.rocketmq.common.message.MessageExt) +Method viewMessage(java.lang.String,java.lang.String) : public throws (org.apache.rocketmq.common.message.MessageExt) +Method wipeWritePermOfBroker(java.lang.String,java.lang.String) : public throws (int) +Method withNamespace(java.lang.String) : public throws (java.lang.String) +Method withNamespace(java.util.Set) : public throws (java.util.Set) +Method withoutNamespace(java.lang.String) : public throws (java.lang.String) +Method withoutNamespace(java.util.Set) : public throws (java.util.Set) diff --git a/test/src/test/resources/schema/protocol/common.protocol.RequestCode.schema b/test/src/test/resources/schema/protocol/common.protocol.RequestCode.schema new file mode 100644 index 0000000..a08162f --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.RequestCode.schema @@ -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. + */ + + +Field ADD_WRITE_PERM_OF_BROKER : public int 327 +Field ADJUST_CONSUMER_THREAD_POOL : public int 213 +Field CHECK_CLIENT_CONFIG : public int 46 +Field CHECK_TRANSACTION_STATE : public int 39 +Field CLEAN_EXPIRED_CONSUMEQUEUE : public int 306 +Field CLEAN_UNUSED_TOPIC : public int 316 +Field CLONE_GROUP_OFFSET : public int 314 +Field CONSUMER_SEND_MSG_BACK : public int 36 +Field CONSUME_MESSAGE_DIRECTLY : public int 309 +Field DELETE_ACL_CONFIG : public int 51 +Field DELETE_EXPIRED_COMMITLOG : public int 329 +Field DELETE_KV_CONFIG : public int 102 +Field DELETE_SUBSCRIPTIONGROUP : public int 207 +Field DELETE_TOPIC_IN_BROKER : public int 215 +Field DELETE_TOPIC_IN_NAMESRV : public int 216 +Field END_TRANSACTION : public int 37 +Field GET_ALL_CONSUMER_OFFSET : public int 43 +Field GET_ALL_DELAY_OFFSET : public int 45 +Field GET_ALL_PRODUCER_INFO : public int 328 +Field GET_ALL_SUBSCRIPTIONGROUP_CONFIG : public int 201 +Field GET_ALL_TOPIC_CONFIG : public int 21 +Field GET_ALL_TOPIC_LIST_FROM_NAMESERVER : public int 206 +Field GET_BROKER_CLUSTER_ACL_CONFIG : public int 54 +Field GET_BROKER_CLUSTER_ACL_INFO : public int 52 +Field GET_BROKER_CLUSTER_INFO : public int 106 +Field GET_BROKER_CONFIG : public int 26 +Field GET_BROKER_CONSUME_STATS : public int 317 +Field GET_BROKER_RUNTIME_INFO : public int 28 +Field GET_CONSUMER_CONNECTION_LIST : public int 203 +Field GET_CONSUMER_LIST_BY_GROUP : public int 38 +Field GET_CONSUMER_RUNNING_INFO : public int 307 +Field GET_CONSUMER_STATUS_FROM_CLIENT : public int 221 +Field GET_CONSUME_STATS : public int 208 +Field GET_EARLIEST_MSG_STORETIME : public int 32 +Field GET_HAS_UNIT_SUB_TOPIC_LIST : public int 312 +Field GET_HAS_UNIT_SUB_UNUNIT_TOPIC_LIST : public int 313 +Field GET_KVLIST_BY_NAMESPACE : public int 219 +Field GET_KV_CONFIG : public int 101 +Field GET_MAX_OFFSET : public int 30 +Field GET_MIN_OFFSET : public int 31 +Field GET_NAMESRV_CONFIG : public int 319 +Field GET_PRODUCER_CONNECTION_LIST : public int 204 +Field GET_ROUTEINFO_BY_TOPIC : public int 105 +Field GET_SYSTEM_TOPIC_LIST_FROM_BROKER : public int 305 +Field GET_SYSTEM_TOPIC_LIST_FROM_NS : public int 304 +Field GET_TOPICS_BY_CLUSTER : public int 224 +Field GET_TOPIC_CONFIG_LIST : public int 22 +Field GET_TOPIC_NAME_LIST : public int 23 +Field GET_TOPIC_STATS_INFO : public int 202 +Field GET_UNIT_TOPIC_LIST : public int 311 +Field HEART_BEAT : public int 34 +Field INVOKE_BROKER_TO_GET_CONSUMER_STATUS : public int 223 +Field INVOKE_BROKER_TO_RESET_OFFSET : public int 222 +Field LOCK_BATCH_MQ : public int 41 +Field NOTIFY_CONSUMER_IDS_CHANGED : public int 40 +Field PULL_MESSAGE : public int 11 +Field PUSH_REPLY_MESSAGE_TO_CLIENT : public int 326 +Field PUT_KV_CONFIG : public int 100 +Field QUERY_BROKER_OFFSET : public int 13 +Field QUERY_CONSUMER_OFFSET : public int 14 +Field QUERY_CONSUME_QUEUE : public int 321 +Field QUERY_CONSUME_TIME_SPAN : public int 303 +Field QUERY_CORRECTION_OFFSET : public int 308 +Field QUERY_DATA_VERSION : public int 322 +Field QUERY_MESSAGE : public int 12 +Field QUERY_TOPIC_CONSUME_BY_WHO : public int 300 +Field REGISTER_BROKER : public int 103 +Field REGISTER_FILTER_SERVER : public int 301 +Field REGISTER_MESSAGE_FILTER_CLASS : public int 302 +Field RESET_CONSUMER_CLIENT_OFFSET : public int 220 +Field RESET_CONSUMER_OFFSET_IN_BROKER : public int 212 +Field RESET_CONSUMER_OFFSET_IN_CONSUMER : public int 211 +Field RESUME_CHECK_HALF_MESSAGE : public int 323 +Field RESUME_CONSUMER : public int 210 +Field SEARCH_OFFSET_BY_TIMESTAMP : public int 29 +Field SEND_BATCH_MESSAGE : public int 320 +Field SEND_MESSAGE : public int 10 +Field SEND_MESSAGE_V2 : public int 310 +Field SEND_REPLY_MESSAGE : public int 324 +Field SEND_REPLY_MESSAGE_V2 : public int 325 +Field SUSPEND_CONSUMER : public int 209 +Field TRIGGER_DELETE_FILES : public int 27 +Field UNLOCK_BATCH_MQ : public int 42 +Field UNREGISTER_BROKER : public int 104 +Field UNREGISTER_CLIENT : public int 35 +Field UPDATE_AND_CREATE_ACL_CONFIG : public int 50 +Field UPDATE_AND_CREATE_SUBSCRIPTIONGROUP : public int 200 +Field UPDATE_AND_CREATE_TOPIC : public int 17 +Field UPDATE_BROKER_CONFIG : public int 25 +Field UPDATE_CONSUMER_OFFSET : public int 15 +Field UPDATE_GLOBAL_WHITE_ADDRS_CONFIG : public int 53 +Field UPDATE_NAMESRV_CONFIG : public int 318 +Field VIEW_BROKER_STATS_DATA : public int 315 +Field VIEW_MESSAGE_BY_ID : public int 33 +Field WHO_CONSUME_THE_MESSAGE : public int 214 +Field WIPE_WRITE_PERM_OF_BROKER : public int 205 diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.CheckTransactionStateRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.CheckTransactionStateRequestHeader.schema new file mode 100644 index 0000000..54c18ef --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.CheckTransactionStateRequestHeader.schema @@ -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. + */ + + +Field commitLogOffset : private java.lang.Long null +Field msgId : private java.lang.String null +Field offsetMsgId : private java.lang.String null +Field tranStateTableOffset : private java.lang.Long null +Field transactionId : private java.lang.String null +Method checkFields() : public throws (void) +Method getCommitLogOffset() : public throws (java.lang.Long) +Method getMsgId() : public throws (java.lang.String) +Method getOffsetMsgId() : public throws (java.lang.String) +Method getTranStateTableOffset() : public throws (java.lang.Long) +Method getTransactionId() : public throws (java.lang.String) +Method setCommitLogOffset(java.lang.Long) : public throws (void) +Method setMsgId(java.lang.String) : public throws (void) +Method setOffsetMsgId(java.lang.String) : public throws (void) +Method setTranStateTableOffset(java.lang.Long) : public throws (void) +Method setTransactionId(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.CheckTransactionStateResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.CheckTransactionStateResponseHeader.schema new file mode 100644 index 0000000..229d33b --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.CheckTransactionStateResponseHeader.schema @@ -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. + */ + + +Field commitLogOffset : private java.lang.Long null +Field commitOrRollback : private java.lang.Integer null +Field producerGroup : private java.lang.String null +Field tranStateTableOffset : private java.lang.Long null +Method checkFields() : public throws (void) +Method getCommitLogOffset() : public throws (java.lang.Long) +Method getCommitOrRollback() : public throws (java.lang.Integer) +Method getProducerGroup() : public throws (java.lang.String) +Method getTranStateTableOffset() : public throws (java.lang.Long) +Method setCommitLogOffset(java.lang.Long) : public throws (void) +Method setCommitOrRollback(java.lang.Integer) : public throws (void) +Method setProducerGroup(java.lang.String) : public throws (void) +Method setTranStateTableOffset(java.lang.Long) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.CloneGroupOffsetRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.CloneGroupOffsetRequestHeader.schema new file mode 100644 index 0000000..fb34086 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.CloneGroupOffsetRequestHeader.schema @@ -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. + */ + + +Field destGroup : private java.lang.String null +Field offline : private boolean false +Field srcGroup : private java.lang.String null +Field topic : private java.lang.String null +Method checkFields() : public throws (void) +Method getDestGroup() : public throws (java.lang.String) +Method getSrcGroup() : public throws (java.lang.String) +Method getTopic() : public throws (java.lang.String) +Method isOffline() : public throws (boolean) +Method setDestGroup(java.lang.String) : public throws (void) +Method setOffline(boolean) : public throws (void) +Method setSrcGroup(java.lang.String) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.ConsumeMessageDirectlyResultRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.ConsumeMessageDirectlyResultRequestHeader.schema new file mode 100644 index 0000000..c0ca8d4 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.ConsumeMessageDirectlyResultRequestHeader.schema @@ -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. + */ + + +Field brokerName : private java.lang.String null +Field clientId : private java.lang.String null +Field consumerGroup : private java.lang.String null +Field msgId : private java.lang.String null +Method checkFields() : public throws (void) +Method getBrokerName() : public throws (java.lang.String) +Method getClientId() : public throws (java.lang.String) +Method getConsumerGroup() : public throws (java.lang.String) +Method getMsgId() : public throws (java.lang.String) +Method setBrokerName(java.lang.String) : public throws (void) +Method setClientId(java.lang.String) : public throws (void) +Method setConsumerGroup(java.lang.String) : public throws (void) +Method setMsgId(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.ConsumerSendMsgBackRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.ConsumerSendMsgBackRequestHeader.schema new file mode 100644 index 0000000..4cbc9cb --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.ConsumerSendMsgBackRequestHeader.schema @@ -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. + */ + + +Field delayLevel : private java.lang.Integer null +Field group : private java.lang.String null +Field maxReconsumeTimes : private java.lang.Integer null +Field offset : private java.lang.Long null +Field originMsgId : private java.lang.String null +Field originTopic : private java.lang.String null +Field unitMode : private boolean false +Method checkFields() : public throws (void) +Method getDelayLevel() : public throws (java.lang.Integer) +Method getGroup() : public throws (java.lang.String) +Method getMaxReconsumeTimes() : public throws (java.lang.Integer) +Method getOffset() : public throws (java.lang.Long) +Method getOriginMsgId() : public throws (java.lang.String) +Method getOriginTopic() : public throws (java.lang.String) +Method isUnitMode() : public throws (boolean) +Method setDelayLevel(java.lang.Integer) : public throws (void) +Method setGroup(java.lang.String) : public throws (void) +Method setMaxReconsumeTimes(java.lang.Integer) : public throws (void) +Method setOffset(java.lang.Long) : public throws (void) +Method setOriginMsgId(java.lang.String) : public throws (void) +Method setOriginTopic(java.lang.String) : public throws (void) +Method setUnitMode(boolean) : public throws (void) +Method toString() : public throws (java.lang.String) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.CreateAccessConfigRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.CreateAccessConfigRequestHeader.schema new file mode 100644 index 0000000..4868645 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.CreateAccessConfigRequestHeader.schema @@ -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. + */ + + +Field accessKey : private java.lang.String null +Field admin : private boolean false +Field defaultGroupPerm : private java.lang.String null +Field defaultTopicPerm : private java.lang.String null +Field groupPerms : private java.lang.String null +Field secretKey : private java.lang.String null +Field topicPerms : private java.lang.String null +Field whiteRemoteAddress : private java.lang.String null +Method checkFields() : public throws (void) +Method getAccessKey() : public throws (java.lang.String) +Method getDefaultGroupPerm() : public throws (java.lang.String) +Method getDefaultTopicPerm() : public throws (java.lang.String) +Method getGroupPerms() : public throws (java.lang.String) +Method getSecretKey() : public throws (java.lang.String) +Method getTopicPerms() : public throws (java.lang.String) +Method getWhiteRemoteAddress() : public throws (java.lang.String) +Method isAdmin() : public throws (boolean) +Method setAccessKey(java.lang.String) : public throws (void) +Method setAdmin(boolean) : public throws (void) +Method setDefaultGroupPerm(java.lang.String) : public throws (void) +Method setDefaultTopicPerm(java.lang.String) : public throws (void) +Method setGroupPerms(java.lang.String) : public throws (void) +Method setSecretKey(java.lang.String) : public throws (void) +Method setTopicPerms(java.lang.String) : public throws (void) +Method setWhiteRemoteAddress(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.CreateTopicRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.CreateTopicRequestHeader.schema new file mode 100644 index 0000000..c8115c3 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.CreateTopicRequestHeader.schema @@ -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. + */ + + +Field defaultTopic : private java.lang.String null +Field order : private java.lang.Boolean false +Field perm : private java.lang.Integer null +Field readQueueNums : private java.lang.Integer null +Field topic : private java.lang.String null +Field topicFilterType : private java.lang.String null +Field topicSysFlag : private java.lang.Integer null +Field writeQueueNums : private java.lang.Integer null +Method checkFields() : public throws (void) +Method getDefaultTopic() : public throws (java.lang.String) +Method getOrder() : public throws (java.lang.Boolean) +Method getPerm() : public throws (java.lang.Integer) +Method getReadQueueNums() : public throws (java.lang.Integer) +Method getTopic() : public throws (java.lang.String) +Method getTopicFilterType() : public throws (java.lang.String) +Method getTopicFilterTypeEnum() : public throws (org.apache.rocketmq.common.TopicFilterType) +Method getTopicSysFlag() : public throws (java.lang.Integer) +Method getWriteQueueNums() : public throws (java.lang.Integer) +Method setDefaultTopic(java.lang.String) : public throws (void) +Method setOrder(java.lang.Boolean) : public throws (void) +Method setPerm(java.lang.Integer) : public throws (void) +Method setReadQueueNums(java.lang.Integer) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) +Method setTopicFilterType(java.lang.String) : public throws (void) +Method setTopicSysFlag(java.lang.Integer) : public throws (void) +Method setWriteQueueNums(java.lang.Integer) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.DeleteAccessConfigRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.DeleteAccessConfigRequestHeader.schema new file mode 100644 index 0000000..58bc858 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.DeleteAccessConfigRequestHeader.schema @@ -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. + */ + + +Field accessKey : private java.lang.String null +Method checkFields() : public throws (void) +Method getAccessKey() : public throws (java.lang.String) +Method setAccessKey(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.DeleteSubscriptionGroupRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.DeleteSubscriptionGroupRequestHeader.schema new file mode 100644 index 0000000..4333679 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.DeleteSubscriptionGroupRequestHeader.schema @@ -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. + */ + + +Field groupName : private java.lang.String null +Field removeOffset : private boolean false +Method checkFields() : public throws (void) +Method getGroupName() : public throws (java.lang.String) +Method isRemoveOffset() : public throws (boolean) +Method setGroupName(java.lang.String) : public throws (void) +Method setRemoveOffset(boolean) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.DeleteTopicRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.DeleteTopicRequestHeader.schema new file mode 100644 index 0000000..fd6809a --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.DeleteTopicRequestHeader.schema @@ -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. + */ + + +Field topic : private java.lang.String null +Method checkFields() : public throws (void) +Method getTopic() : public throws (java.lang.String) +Method setTopic(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.EndTransactionRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.EndTransactionRequestHeader.schema new file mode 100644 index 0000000..e966d87 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.EndTransactionRequestHeader.schema @@ -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. + */ + + +Field commitLogOffset : private java.lang.Long null +Field commitOrRollback : private java.lang.Integer null +Field fromTransactionCheck : private java.lang.Boolean false +Field msgId : private java.lang.String null +Field producerGroup : private java.lang.String null +Field tranStateTableOffset : private java.lang.Long null +Field transactionId : private java.lang.String null +Method checkFields() : public throws (void) +Method getCommitLogOffset() : public throws (java.lang.Long) +Method getCommitOrRollback() : public throws (java.lang.Integer) +Method getFromTransactionCheck() : public throws (java.lang.Boolean) +Method getMsgId() : public throws (java.lang.String) +Method getProducerGroup() : public throws (java.lang.String) +Method getTranStateTableOffset() : public throws (java.lang.Long) +Method getTransactionId() : public throws (java.lang.String) +Method setCommitLogOffset(java.lang.Long) : public throws (void) +Method setCommitOrRollback(java.lang.Integer) : public throws (void) +Method setFromTransactionCheck(java.lang.Boolean) : public throws (void) +Method setMsgId(java.lang.String) : public throws (void) +Method setProducerGroup(java.lang.String) : public throws (void) +Method setTranStateTableOffset(java.lang.Long) : public throws (void) +Method setTransactionId(java.lang.String) : public throws (void) +Method toString() : public throws (java.lang.String) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.EndTransactionResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.EndTransactionResponseHeader.schema new file mode 100644 index 0000000..bf62cfb --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.EndTransactionResponseHeader.schema @@ -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. + */ + + +Method checkFields() : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.GetAllProducerInfoRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.GetAllProducerInfoRequestHeader.schema new file mode 100644 index 0000000..bf62cfb --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.GetAllProducerInfoRequestHeader.schema @@ -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. + */ + + +Method checkFields() : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.GetAllTopicConfigResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.GetAllTopicConfigResponseHeader.schema new file mode 100644 index 0000000..bf62cfb --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.GetAllTopicConfigResponseHeader.schema @@ -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. + */ + + +Method checkFields() : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.GetBrokerAclConfigResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.GetBrokerAclConfigResponseHeader.schema new file mode 100644 index 0000000..41818f0 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.GetBrokerAclConfigResponseHeader.schema @@ -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. + */ + + +Field allAclFileVersion : private java.lang.String null +Field brokerAddr : private java.lang.String null +Field brokerName : private java.lang.String null +Field clusterName : private java.lang.String null +Field version : private java.lang.String null +Method checkFields() : public throws (void) +Method getAllAclFileVersion() : public throws (java.lang.String) +Method getBrokerAddr() : public throws (java.lang.String) +Method getBrokerName() : public throws (java.lang.String) +Method getClusterName() : public throws (java.lang.String) +Method getVersion() : public throws (java.lang.String) +Method setAllAclFileVersion(java.lang.String) : public throws (void) +Method setBrokerAddr(java.lang.String) : public throws (void) +Method setBrokerName(java.lang.String) : public throws (void) +Method setClusterName(java.lang.String) : public throws (void) +Method setVersion(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.GetBrokerClusterAclConfigResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.GetBrokerClusterAclConfigResponseHeader.schema new file mode 100644 index 0000000..a13d5ae --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.GetBrokerClusterAclConfigResponseHeader.schema @@ -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. + */ + + +Field plainAccessConfigs : private java.util.List null +Method checkFields() : public throws (void) +Method getPlainAccessConfigs() : public throws (java.util.List) +Method setPlainAccessConfigs(java.util.List) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.GetBrokerConfigResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.GetBrokerConfigResponseHeader.schema new file mode 100644 index 0000000..9500091 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.GetBrokerConfigResponseHeader.schema @@ -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. + */ + + +Field version : private java.lang.String null +Method checkFields() : public throws (void) +Method getVersion() : public throws (java.lang.String) +Method setVersion(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.GetConsumeStatsInBrokerHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.GetConsumeStatsInBrokerHeader.schema new file mode 100644 index 0000000..b1785e9 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.GetConsumeStatsInBrokerHeader.schema @@ -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. + */ + + +Field isOrder : private boolean false +Method checkFields() : public throws (void) +Method isOrder() : public throws (boolean) +Method setIsOrder(boolean) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.GetConsumeStatsRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.GetConsumeStatsRequestHeader.schema new file mode 100644 index 0000000..d74f24e --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.GetConsumeStatsRequestHeader.schema @@ -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. + */ + + +Field consumerGroup : private java.lang.String null +Field topic : private java.lang.String null +Method checkFields() : public throws (void) +Method getConsumerGroup() : public throws (java.lang.String) +Method getTopic() : public throws (java.lang.String) +Method setConsumerGroup(java.lang.String) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.GetConsumerConnectionListRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.GetConsumerConnectionListRequestHeader.schema new file mode 100644 index 0000000..9a8061a --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.GetConsumerConnectionListRequestHeader.schema @@ -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. + */ + + +Field consumerGroup : private java.lang.String null +Method checkFields() : public throws (void) +Method getConsumerGroup() : public throws (java.lang.String) +Method setConsumerGroup(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.GetConsumerListByGroupRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.GetConsumerListByGroupRequestHeader.schema new file mode 100644 index 0000000..9a8061a --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.GetConsumerListByGroupRequestHeader.schema @@ -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. + */ + + +Field consumerGroup : private java.lang.String null +Method checkFields() : public throws (void) +Method getConsumerGroup() : public throws (java.lang.String) +Method setConsumerGroup(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.GetConsumerListByGroupResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.GetConsumerListByGroupResponseHeader.schema new file mode 100644 index 0000000..bf62cfb --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.GetConsumerListByGroupResponseHeader.schema @@ -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. + */ + + +Method checkFields() : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.GetConsumerRunningInfoRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.GetConsumerRunningInfoRequestHeader.schema new file mode 100644 index 0000000..787ef50 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.GetConsumerRunningInfoRequestHeader.schema @@ -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. + */ + + +Field clientId : private java.lang.String null +Field consumerGroup : private java.lang.String null +Field jstackEnable : private boolean false +Method checkFields() : public throws (void) +Method getClientId() : public throws (java.lang.String) +Method getConsumerGroup() : public throws (java.lang.String) +Method isJstackEnable() : public throws (boolean) +Method setClientId(java.lang.String) : public throws (void) +Method setConsumerGroup(java.lang.String) : public throws (void) +Method setJstackEnable(boolean) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.GetConsumerStatusRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.GetConsumerStatusRequestHeader.schema new file mode 100644 index 0000000..58ae3c2 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.GetConsumerStatusRequestHeader.schema @@ -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. + */ + + +Field clientAddr : private java.lang.String null +Field group : private java.lang.String null +Field topic : private java.lang.String null +Method checkFields() : public throws (void) +Method getClientAddr() : public throws (java.lang.String) +Method getGroup() : public throws (java.lang.String) +Method getTopic() : public throws (java.lang.String) +Method setClientAddr(java.lang.String) : public throws (void) +Method setGroup(java.lang.String) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.GetEarliestMsgStoretimeRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.GetEarliestMsgStoretimeRequestHeader.schema new file mode 100644 index 0000000..2c7520c --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.GetEarliestMsgStoretimeRequestHeader.schema @@ -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. + */ + + +Field queueId : private java.lang.Integer null +Field topic : private java.lang.String null +Method checkFields() : public throws (void) +Method getQueueId() : public throws (java.lang.Integer) +Method getTopic() : public throws (java.lang.String) +Method setQueueId(java.lang.Integer) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.GetEarliestMsgStoretimeResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.GetEarliestMsgStoretimeResponseHeader.schema new file mode 100644 index 0000000..e99308a --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.GetEarliestMsgStoretimeResponseHeader.schema @@ -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. + */ + + +Field timestamp : private java.lang.Long null +Method checkFields() : public throws (void) +Method getTimestamp() : public throws (java.lang.Long) +Method setTimestamp(java.lang.Long) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.GetMaxOffsetRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.GetMaxOffsetRequestHeader.schema new file mode 100644 index 0000000..2c7520c --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.GetMaxOffsetRequestHeader.schema @@ -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. + */ + + +Field queueId : private java.lang.Integer null +Field topic : private java.lang.String null +Method checkFields() : public throws (void) +Method getQueueId() : public throws (java.lang.Integer) +Method getTopic() : public throws (java.lang.String) +Method setQueueId(java.lang.Integer) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.GetMaxOffsetResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.GetMaxOffsetResponseHeader.schema new file mode 100644 index 0000000..1b24f4f --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.GetMaxOffsetResponseHeader.schema @@ -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. + */ + + +Field offset : private java.lang.Long null +Method checkFields() : public throws (void) +Method getOffset() : public throws (java.lang.Long) +Method setOffset(java.lang.Long) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.GetMinOffsetRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.GetMinOffsetRequestHeader.schema new file mode 100644 index 0000000..2c7520c --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.GetMinOffsetRequestHeader.schema @@ -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. + */ + + +Field queueId : private java.lang.Integer null +Field topic : private java.lang.String null +Method checkFields() : public throws (void) +Method getQueueId() : public throws (java.lang.Integer) +Method getTopic() : public throws (java.lang.String) +Method setQueueId(java.lang.Integer) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.GetMinOffsetResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.GetMinOffsetResponseHeader.schema new file mode 100644 index 0000000..1b24f4f --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.GetMinOffsetResponseHeader.schema @@ -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. + */ + + +Field offset : private java.lang.Long null +Method checkFields() : public throws (void) +Method getOffset() : public throws (java.lang.Long) +Method setOffset(java.lang.Long) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.GetProducerConnectionListRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.GetProducerConnectionListRequestHeader.schema new file mode 100644 index 0000000..f595a98 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.GetProducerConnectionListRequestHeader.schema @@ -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. + */ + + +Field producerGroup : private java.lang.String null +Method checkFields() : public throws (void) +Method getProducerGroup() : public throws (java.lang.String) +Method setProducerGroup(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.GetTopicStatsInfoRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.GetTopicStatsInfoRequestHeader.schema new file mode 100644 index 0000000..fd6809a --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.GetTopicStatsInfoRequestHeader.schema @@ -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. + */ + + +Field topic : private java.lang.String null +Method checkFields() : public throws (void) +Method getTopic() : public throws (java.lang.String) +Method setTopic(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.GetTopicsByClusterRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.GetTopicsByClusterRequestHeader.schema new file mode 100644 index 0000000..493f549 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.GetTopicsByClusterRequestHeader.schema @@ -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. + */ + + +Field cluster : private java.lang.String null +Method checkFields() : public throws (void) +Method getCluster() : public throws (java.lang.String) +Method setCluster(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.NotifyConsumerIdsChangedRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.NotifyConsumerIdsChangedRequestHeader.schema new file mode 100644 index 0000000..9a8061a --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.NotifyConsumerIdsChangedRequestHeader.schema @@ -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. + */ + + +Field consumerGroup : private java.lang.String null +Method checkFields() : public throws (void) +Method getConsumerGroup() : public throws (java.lang.String) +Method setConsumerGroup(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.PullMessageRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.PullMessageRequestHeader.schema new file mode 100644 index 0000000..7507584 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.PullMessageRequestHeader.schema @@ -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. + */ + + +Field commitOffset : private java.lang.Long null +Field consumerGroup : private java.lang.String null +Field expressionType : private java.lang.String null +Field maxMsgNums : private java.lang.Integer null +Field queueId : private java.lang.Integer null +Field queueOffset : private java.lang.Long null +Field subVersion : private java.lang.Long null +Field subscription : private java.lang.String null +Field suspendTimeoutMillis : private java.lang.Long null +Field sysFlag : private java.lang.Integer null +Field topic : private java.lang.String null +Method checkFields() : public throws (void) +Method decode(java.util.HashMap) : public throws (void) +Method encode(io.netty.buffer.ByteBuf) : public throws (void) +Method getCommitOffset() : public throws (java.lang.Long) +Method getConsumerGroup() : public throws (java.lang.String) +Method getExpressionType() : public throws (java.lang.String) +Method getMaxMsgNums() : public throws (java.lang.Integer) +Method getQueueId() : public throws (java.lang.Integer) +Method getQueueOffset() : public throws (java.lang.Long) +Method getSubVersion() : public throws (java.lang.Long) +Method getSubscription() : public throws (java.lang.String) +Method getSuspendTimeoutMillis() : public throws (java.lang.Long) +Method getSysFlag() : public throws (java.lang.Integer) +Method getTopic() : public throws (java.lang.String) +Method setCommitOffset(java.lang.Long) : public throws (void) +Method setConsumerGroup(java.lang.String) : public throws (void) +Method setExpressionType(java.lang.String) : public throws (void) +Method setMaxMsgNums(java.lang.Integer) : public throws (void) +Method setQueueId(java.lang.Integer) : public throws (void) +Method setQueueOffset(java.lang.Long) : public throws (void) +Method setSubVersion(java.lang.Long) : public throws (void) +Method setSubscription(java.lang.String) : public throws (void) +Method setSuspendTimeoutMillis(java.lang.Long) : public throws (void) +Method setSysFlag(java.lang.Integer) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.PullMessageResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.PullMessageResponseHeader.schema new file mode 100644 index 0000000..01a7fab --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.PullMessageResponseHeader.schema @@ -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. + */ + + +Field maxOffset : private java.lang.Long null +Field minOffset : private java.lang.Long null +Field nextBeginOffset : private java.lang.Long null +Field suggestWhichBrokerId : private java.lang.Long null +Method checkFields() : public throws (void) +Method decode(java.util.HashMap) : public throws (void) +Method encode(io.netty.buffer.ByteBuf) : public throws (void) +Method getMaxOffset() : public throws (java.lang.Long) +Method getMinOffset() : public throws (java.lang.Long) +Method getNextBeginOffset() : public throws (java.lang.Long) +Method getSuggestWhichBrokerId() : public throws (java.lang.Long) +Method setMaxOffset(java.lang.Long) : public throws (void) +Method setMinOffset(java.lang.Long) : public throws (void) +Method setNextBeginOffset(java.lang.Long) : public throws (void) +Method setSuggestWhichBrokerId(java.lang.Long) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.QueryConsumeQueueRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.QueryConsumeQueueRequestHeader.schema new file mode 100644 index 0000000..c54ebf8 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.QueryConsumeQueueRequestHeader.schema @@ -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. + */ + + +Field consumerGroup : private java.lang.String null +Field count : private int 0 +Field index : private long 0 +Field queueId : private int 0 +Field topic : private java.lang.String null +Method checkFields() : public throws (void) +Method getConsumerGroup() : public throws (java.lang.String) +Method getCount() : public throws (int) +Method getIndex() : public throws (long) +Method getQueueId() : public throws (int) +Method getTopic() : public throws (java.lang.String) +Method setConsumerGroup(java.lang.String) : public throws (void) +Method setCount(int) : public throws (void) +Method setIndex(long) : public throws (void) +Method setQueueId(int) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.QueryConsumeTimeSpanRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.QueryConsumeTimeSpanRequestHeader.schema new file mode 100644 index 0000000..003ce1e --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.QueryConsumeTimeSpanRequestHeader.schema @@ -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. + */ + + +Field group : private java.lang.String null +Field topic : private java.lang.String null +Method checkFields() : public throws (void) +Method getGroup() : public throws (java.lang.String) +Method getTopic() : public throws (java.lang.String) +Method setGroup(java.lang.String) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.QueryConsumerOffsetRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.QueryConsumerOffsetRequestHeader.schema new file mode 100644 index 0000000..617dea8 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.QueryConsumerOffsetRequestHeader.schema @@ -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. + */ + + +Field consumerGroup : private java.lang.String null +Field queueId : private java.lang.Integer null +Field topic : private java.lang.String null +Method checkFields() : public throws (void) +Method getConsumerGroup() : public throws (java.lang.String) +Method getQueueId() : public throws (java.lang.Integer) +Method getTopic() : public throws (java.lang.String) +Method setConsumerGroup(java.lang.String) : public throws (void) +Method setQueueId(java.lang.Integer) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.QueryConsumerOffsetResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.QueryConsumerOffsetResponseHeader.schema new file mode 100644 index 0000000..1b24f4f --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.QueryConsumerOffsetResponseHeader.schema @@ -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. + */ + + +Field offset : private java.lang.Long null +Method checkFields() : public throws (void) +Method getOffset() : public throws (java.lang.Long) +Method setOffset(java.lang.Long) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.QueryCorrectionOffsetHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.QueryCorrectionOffsetHeader.schema new file mode 100644 index 0000000..39f3c81 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.QueryCorrectionOffsetHeader.schema @@ -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. + */ + + +Field compareGroup : private java.lang.String null +Field filterGroups : private java.lang.String null +Field topic : private java.lang.String null +Method checkFields() : public throws (void) +Method getCompareGroup() : public throws (java.lang.String) +Method getFilterGroups() : public throws (java.lang.String) +Method getTopic() : public throws (java.lang.String) +Method setCompareGroup(java.lang.String) : public throws (void) +Method setFilterGroups(java.lang.String) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.QueryMessageRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.QueryMessageRequestHeader.schema new file mode 100644 index 0000000..2ed333e --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.QueryMessageRequestHeader.schema @@ -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. + */ + + +Field beginTimestamp : private java.lang.Long null +Field endTimestamp : private java.lang.Long null +Field key : private java.lang.String null +Field maxNum : private java.lang.Integer null +Field topic : private java.lang.String null +Method checkFields() : public throws (void) +Method getBeginTimestamp() : public throws (java.lang.Long) +Method getEndTimestamp() : public throws (java.lang.Long) +Method getKey() : public throws (java.lang.String) +Method getMaxNum() : public throws (java.lang.Integer) +Method getTopic() : public throws (java.lang.String) +Method setBeginTimestamp(java.lang.Long) : public throws (void) +Method setEndTimestamp(java.lang.Long) : public throws (void) +Method setKey(java.lang.String) : public throws (void) +Method setMaxNum(java.lang.Integer) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.QueryMessageResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.QueryMessageResponseHeader.schema new file mode 100644 index 0000000..1d27695 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.QueryMessageResponseHeader.schema @@ -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. + */ + + +Field indexLastUpdatePhyoffset : private java.lang.Long null +Field indexLastUpdateTimestamp : private java.lang.Long null +Method checkFields() : public throws (void) +Method getIndexLastUpdatePhyoffset() : public throws (java.lang.Long) +Method getIndexLastUpdateTimestamp() : public throws (java.lang.Long) +Method setIndexLastUpdatePhyoffset(java.lang.Long) : public throws (void) +Method setIndexLastUpdateTimestamp(java.lang.Long) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.QueryTopicConsumeByWhoRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.QueryTopicConsumeByWhoRequestHeader.schema new file mode 100644 index 0000000..fd6809a --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.QueryTopicConsumeByWhoRequestHeader.schema @@ -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. + */ + + +Field topic : private java.lang.String null +Method checkFields() : public throws (void) +Method getTopic() : public throws (java.lang.String) +Method setTopic(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.ReplyMessageRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.ReplyMessageRequestHeader.schema new file mode 100644 index 0000000..a83714f --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.ReplyMessageRequestHeader.schema @@ -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. + */ + + +Field bornHost : private java.lang.String null +Field bornTimestamp : private java.lang.Long null +Field defaultTopic : private java.lang.String null +Field defaultTopicQueueNums : private java.lang.Integer null +Field flag : private java.lang.Integer null +Field producerGroup : private java.lang.String null +Field properties : private java.lang.String null +Field queueId : private java.lang.Integer null +Field reconsumeTimes : private java.lang.Integer null +Field storeHost : private java.lang.String null +Field storeTimestamp : private long 0 +Field sysFlag : private java.lang.Integer null +Field topic : private java.lang.String null +Field unitMode : private boolean false +Method checkFields() : public throws (void) +Method getBornHost() : public throws (java.lang.String) +Method getBornTimestamp() : public throws (java.lang.Long) +Method getDefaultTopic() : public throws (java.lang.String) +Method getDefaultTopicQueueNums() : public throws (java.lang.Integer) +Method getFlag() : public throws (java.lang.Integer) +Method getProducerGroup() : public throws (java.lang.String) +Method getProperties() : public throws (java.lang.String) +Method getQueueId() : public throws (java.lang.Integer) +Method getReconsumeTimes() : public throws (java.lang.Integer) +Method getStoreHost() : public throws (java.lang.String) +Method getStoreTimestamp() : public throws (long) +Method getSysFlag() : public throws (java.lang.Integer) +Method getTopic() : public throws (java.lang.String) +Method isUnitMode() : public throws (boolean) +Method setBornHost(java.lang.String) : public throws (void) +Method setBornTimestamp(java.lang.Long) : public throws (void) +Method setDefaultTopic(java.lang.String) : public throws (void) +Method setDefaultTopicQueueNums(java.lang.Integer) : public throws (void) +Method setFlag(java.lang.Integer) : public throws (void) +Method setProducerGroup(java.lang.String) : public throws (void) +Method setProperties(java.lang.String) : public throws (void) +Method setQueueId(java.lang.Integer) : public throws (void) +Method setReconsumeTimes(java.lang.Integer) : public throws (void) +Method setStoreHost(java.lang.String) : public throws (void) +Method setStoreTimestamp(long) : public throws (void) +Method setSysFlag(java.lang.Integer) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) +Method setUnitMode(boolean) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.ResetOffsetRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.ResetOffsetRequestHeader.schema new file mode 100644 index 0000000..81c96d7 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.ResetOffsetRequestHeader.schema @@ -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. + */ + + +Field group : private java.lang.String null +Field isForce : private boolean false +Field timestamp : private long 0 +Field topic : private java.lang.String null +Method checkFields() : public throws (void) +Method getGroup() : public throws (java.lang.String) +Method getTimestamp() : public throws (long) +Method getTopic() : public throws (java.lang.String) +Method isForce() : public throws (boolean) +Method setForce(boolean) : public throws (void) +Method setGroup(java.lang.String) : public throws (void) +Method setTimestamp(long) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.ResumeCheckHalfMessageRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.ResumeCheckHalfMessageRequestHeader.schema new file mode 100644 index 0000000..f55a616 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.ResumeCheckHalfMessageRequestHeader.schema @@ -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. + */ + + +Field msgId : private java.lang.String null +Method checkFields() : public throws (void) +Method getMsgId() : public throws (java.lang.String) +Method setMsgId(java.lang.String) : public throws (void) +Method toString() : public throws (java.lang.String) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.SearchOffsetRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.SearchOffsetRequestHeader.schema new file mode 100644 index 0000000..eaf682f --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.SearchOffsetRequestHeader.schema @@ -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. + */ + + +Field queueId : private java.lang.Integer null +Field timestamp : private java.lang.Long null +Field topic : private java.lang.String null +Method checkFields() : public throws (void) +Method getQueueId() : public throws (java.lang.Integer) +Method getTimestamp() : public throws (java.lang.Long) +Method getTopic() : public throws (java.lang.String) +Method setQueueId(java.lang.Integer) : public throws (void) +Method setTimestamp(java.lang.Long) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.SearchOffsetResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.SearchOffsetResponseHeader.schema new file mode 100644 index 0000000..1b24f4f --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.SearchOffsetResponseHeader.schema @@ -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. + */ + + +Field offset : private java.lang.Long null +Method checkFields() : public throws (void) +Method getOffset() : public throws (java.lang.Long) +Method setOffset(java.lang.Long) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.SendMessageRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.SendMessageRequestHeader.schema new file mode 100644 index 0000000..a25b4ea --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.SendMessageRequestHeader.schema @@ -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. + */ + + +Field batch : private boolean false +Field bornTimestamp : private java.lang.Long null +Field defaultTopic : private java.lang.String null +Field defaultTopicQueueNums : private java.lang.Integer null +Field flag : private java.lang.Integer null +Field maxReconsumeTimes : private java.lang.Integer null +Field producerGroup : private java.lang.String null +Field properties : private java.lang.String null +Field queueId : private java.lang.Integer null +Field reconsumeTimes : private java.lang.Integer null +Field sysFlag : private java.lang.Integer null +Field topic : private java.lang.String null +Field unitMode : private boolean false +Method checkFields() : public throws (void) +Method getBornTimestamp() : public throws (java.lang.Long) +Method getDefaultTopic() : public throws (java.lang.String) +Method getDefaultTopicQueueNums() : public throws (java.lang.Integer) +Method getFlag() : public throws (java.lang.Integer) +Method getMaxReconsumeTimes() : public throws (java.lang.Integer) +Method getProducerGroup() : public throws (java.lang.String) +Method getProperties() : public throws (java.lang.String) +Method getQueueId() : public throws (java.lang.Integer) +Method getReconsumeTimes() : public throws (java.lang.Integer) +Method getSysFlag() : public throws (java.lang.Integer) +Method getTopic() : public throws (java.lang.String) +Method isBatch() : public throws (boolean) +Method isUnitMode() : public throws (boolean) +Method setBatch(boolean) : public throws (void) +Method setBornTimestamp(java.lang.Long) : public throws (void) +Method setDefaultTopic(java.lang.String) : public throws (void) +Method setDefaultTopicQueueNums(java.lang.Integer) : public throws (void) +Method setFlag(java.lang.Integer) : public throws (void) +Method setMaxReconsumeTimes(java.lang.Integer) : public throws (void) +Method setProducerGroup(java.lang.String) : public throws (void) +Method setProperties(java.lang.String) : public throws (void) +Method setQueueId(java.lang.Integer) : public throws (void) +Method setReconsumeTimes(java.lang.Integer) : public throws (void) +Method setSysFlag(java.lang.Integer) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) +Method setUnitMode(boolean) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.SendMessageRequestHeaderV2.schema b/test/src/test/resources/schema/protocol/common.protocol.header.SendMessageRequestHeaderV2.schema new file mode 100644 index 0000000..286747a --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.SendMessageRequestHeaderV2.schema @@ -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. + */ + + +Field a : private java.lang.String null +Field b : private java.lang.String null +Field c : private java.lang.String null +Field d : private java.lang.Integer null +Field e : private java.lang.Integer null +Field f : private java.lang.Integer null +Field g : private java.lang.Long null +Field h : private java.lang.Integer null +Field i : private java.lang.String null +Field j : private java.lang.Integer null +Field k : private boolean false +Field l : private java.lang.Integer null +Field m : private boolean false +Method checkFields() : public throws (void) +Method createSendMessageRequestHeaderV1(org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeaderV2) : public throws (org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader) +Method createSendMessageRequestHeaderV2(org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader) : public throws (org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeaderV2) +Method decode(java.util.HashMap) : public throws (void) +Method encode(io.netty.buffer.ByteBuf) : public throws (void) +Method getA() : public throws (java.lang.String) +Method getB() : public throws (java.lang.String) +Method getC() : public throws (java.lang.String) +Method getD() : public throws (java.lang.Integer) +Method getE() : public throws (java.lang.Integer) +Method getF() : public throws (java.lang.Integer) +Method getG() : public throws (java.lang.Long) +Method getH() : public throws (java.lang.Integer) +Method getI() : public throws (java.lang.String) +Method getJ() : public throws (java.lang.Integer) +Method getL() : public throws (java.lang.Integer) +Method isK() : public throws (boolean) +Method isM() : public throws (boolean) +Method setA(java.lang.String) : public throws (void) +Method setB(java.lang.String) : public throws (void) +Method setC(java.lang.String) : public throws (void) +Method setD(java.lang.Integer) : public throws (void) +Method setE(java.lang.Integer) : public throws (void) +Method setF(java.lang.Integer) : public throws (void) +Method setG(java.lang.Long) : public throws (void) +Method setH(java.lang.Integer) : public throws (void) +Method setI(java.lang.String) : public throws (void) +Method setJ(java.lang.Integer) : public throws (void) +Method setK(boolean) : public throws (void) +Method setL(java.lang.Integer) : public throws (void) +Method setM(boolean) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.SendMessageResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.SendMessageResponseHeader.schema new file mode 100644 index 0000000..81e2658 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.SendMessageResponseHeader.schema @@ -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. + */ + + +Field msgId : private java.lang.String null +Field queueId : private java.lang.Integer null +Field queueOffset : private java.lang.Long null +Field transactionId : private java.lang.String null +Method checkFields() : public throws (void) +Method decode(java.util.HashMap) : public throws (void) +Method encode(io.netty.buffer.ByteBuf) : public throws (void) +Method getMsgId() : public throws (java.lang.String) +Method getQueueId() : public throws (java.lang.Integer) +Method getQueueOffset() : public throws (java.lang.Long) +Method getTransactionId() : public throws (java.lang.String) +Method setMsgId(java.lang.String) : public throws (void) +Method setQueueId(java.lang.Integer) : public throws (void) +Method setQueueOffset(java.lang.Long) : public throws (void) +Method setTransactionId(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.UnregisterClientRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.UnregisterClientRequestHeader.schema new file mode 100644 index 0000000..0c6ca0c --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.UnregisterClientRequestHeader.schema @@ -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. + */ + + +Field clientID : private java.lang.String null +Field consumerGroup : private java.lang.String null +Field producerGroup : private java.lang.String null +Method checkFields() : public throws (void) +Method getClientID() : public throws (java.lang.String) +Method getConsumerGroup() : public throws (java.lang.String) +Method getProducerGroup() : public throws (java.lang.String) +Method setClientID(java.lang.String) : public throws (void) +Method setConsumerGroup(java.lang.String) : public throws (void) +Method setProducerGroup(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.UnregisterClientResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.UnregisterClientResponseHeader.schema new file mode 100644 index 0000000..bf62cfb --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.UnregisterClientResponseHeader.schema @@ -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. + */ + + +Method checkFields() : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.UpdateConsumerOffsetRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.UpdateConsumerOffsetRequestHeader.schema new file mode 100644 index 0000000..a1cc444 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.UpdateConsumerOffsetRequestHeader.schema @@ -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. + */ + + +Field commitOffset : private java.lang.Long null +Field consumerGroup : private java.lang.String null +Field queueId : private java.lang.Integer null +Field topic : private java.lang.String null +Method checkFields() : public throws (void) +Method getCommitOffset() : public throws (java.lang.Long) +Method getConsumerGroup() : public throws (java.lang.String) +Method getQueueId() : public throws (java.lang.Integer) +Method getTopic() : public throws (java.lang.String) +Method setCommitOffset(java.lang.Long) : public throws (void) +Method setConsumerGroup(java.lang.String) : public throws (void) +Method setQueueId(java.lang.Integer) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.UpdateConsumerOffsetResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.UpdateConsumerOffsetResponseHeader.schema new file mode 100644 index 0000000..bf62cfb --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.UpdateConsumerOffsetResponseHeader.schema @@ -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. + */ + + +Method checkFields() : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.UpdateGlobalWhiteAddrsConfigRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.UpdateGlobalWhiteAddrsConfigRequestHeader.schema new file mode 100644 index 0000000..0740978 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.UpdateGlobalWhiteAddrsConfigRequestHeader.schema @@ -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. + */ + + +Field aclFileFullPath : private java.lang.String null +Field globalWhiteAddrs : private java.lang.String null +Method checkFields() : public throws (void) +Method getAclFileFullPath() : public throws (java.lang.String) +Method getGlobalWhiteAddrs() : public throws (java.lang.String) +Method setAclFileFullPath(java.lang.String) : public throws (void) +Method setGlobalWhiteAddrs(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.ViewBrokerStatsDataRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.ViewBrokerStatsDataRequestHeader.schema new file mode 100644 index 0000000..da6603d --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.ViewBrokerStatsDataRequestHeader.schema @@ -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. + */ + + +Field statsKey : private java.lang.String null +Field statsName : private java.lang.String null +Method checkFields() : public throws (void) +Method getStatsKey() : public throws (java.lang.String) +Method getStatsName() : public throws (java.lang.String) +Method setStatsKey(java.lang.String) : public throws (void) +Method setStatsName(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.ViewMessageRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.ViewMessageRequestHeader.schema new file mode 100644 index 0000000..1b24f4f --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.ViewMessageRequestHeader.schema @@ -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. + */ + + +Field offset : private java.lang.Long null +Method checkFields() : public throws (void) +Method getOffset() : public throws (java.lang.Long) +Method setOffset(java.lang.Long) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.ViewMessageResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.ViewMessageResponseHeader.schema new file mode 100644 index 0000000..bf62cfb --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.ViewMessageResponseHeader.schema @@ -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. + */ + + +Method checkFields() : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.filtersrv.RegisterFilterServerRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.filtersrv.RegisterFilterServerRequestHeader.schema new file mode 100644 index 0000000..16f5551 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.filtersrv.RegisterFilterServerRequestHeader.schema @@ -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. + */ + + +Field filterServerAddr : private java.lang.String null +Method checkFields() : public throws (void) +Method getFilterServerAddr() : public throws (java.lang.String) +Method setFilterServerAddr(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.filtersrv.RegisterFilterServerResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.filtersrv.RegisterFilterServerResponseHeader.schema new file mode 100644 index 0000000..4257d15 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.filtersrv.RegisterFilterServerResponseHeader.schema @@ -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. + */ + + +Field brokerId : private long 0 +Field brokerName : private java.lang.String null +Method checkFields() : public throws (void) +Method getBrokerId() : public throws (long) +Method getBrokerName() : public throws (java.lang.String) +Method setBrokerId(long) : public throws (void) +Method setBrokerName(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.filtersrv.RegisterMessageFilterClassRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.filtersrv.RegisterMessageFilterClassRequestHeader.schema new file mode 100644 index 0000000..70ce9a6 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.filtersrv.RegisterMessageFilterClassRequestHeader.schema @@ -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. + */ + + +Field classCRC : private java.lang.Integer null +Field className : private java.lang.String null +Field consumerGroup : private java.lang.String null +Field topic : private java.lang.String null +Method checkFields() : public throws (void) +Method getClassCRC() : public throws (java.lang.Integer) +Method getClassName() : public throws (java.lang.String) +Method getConsumerGroup() : public throws (java.lang.String) +Method getTopic() : public throws (java.lang.String) +Method setClassCRC(java.lang.Integer) : public throws (void) +Method setClassName(java.lang.String) : public throws (void) +Method setConsumerGroup(java.lang.String) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.AddWritePermOfBrokerRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.AddWritePermOfBrokerRequestHeader.schema new file mode 100644 index 0000000..6fd2eae --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.AddWritePermOfBrokerRequestHeader.schema @@ -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. + */ + + +Field brokerName : private java.lang.String null +Method checkFields() : public throws (void) +Method getBrokerName() : public throws (java.lang.String) +Method setBrokerName(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.AddWritePermOfBrokerResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.AddWritePermOfBrokerResponseHeader.schema new file mode 100644 index 0000000..f7e3be3 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.AddWritePermOfBrokerResponseHeader.schema @@ -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. + */ + + +Field addTopicCount : private java.lang.Integer null +Method checkFields() : public throws (void) +Method getAddTopicCount() : public throws (java.lang.Integer) +Method setAddTopicCount(java.lang.Integer) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.DeleteKVConfigRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.DeleteKVConfigRequestHeader.schema new file mode 100644 index 0000000..f32a333 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.DeleteKVConfigRequestHeader.schema @@ -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. + */ + + +Field key : private java.lang.String null +Field namespace : private java.lang.String null +Method checkFields() : public throws (void) +Method getKey() : public throws (java.lang.String) +Method getNamespace() : public throws (java.lang.String) +Method setKey(java.lang.String) : public throws (void) +Method setNamespace(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.DeleteTopicFromNamesrvRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.DeleteTopicFromNamesrvRequestHeader.schema new file mode 100644 index 0000000..00a17bf --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.DeleteTopicFromNamesrvRequestHeader.schema @@ -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. + */ + + +Field clusterName : private java.lang.String null +Field topic : private java.lang.String null +Method checkFields() : public throws (void) +Method getClusterName() : public throws (java.lang.String) +Method getTopic() : public throws (java.lang.String) +Method setClusterName(java.lang.String) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.GetKVConfigRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.GetKVConfigRequestHeader.schema new file mode 100644 index 0000000..f32a333 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.GetKVConfigRequestHeader.schema @@ -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. + */ + + +Field key : private java.lang.String null +Field namespace : private java.lang.String null +Method checkFields() : public throws (void) +Method getKey() : public throws (java.lang.String) +Method getNamespace() : public throws (java.lang.String) +Method setKey(java.lang.String) : public throws (void) +Method setNamespace(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.GetKVConfigResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.GetKVConfigResponseHeader.schema new file mode 100644 index 0000000..047a6bb --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.GetKVConfigResponseHeader.schema @@ -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. + */ + + +Field value : private java.lang.String null +Method checkFields() : public throws (void) +Method getValue() : public throws (java.lang.String) +Method setValue(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.GetKVListByNamespaceRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.GetKVListByNamespaceRequestHeader.schema new file mode 100644 index 0000000..8576dd5 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.GetKVListByNamespaceRequestHeader.schema @@ -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. + */ + + +Field namespace : private java.lang.String null +Method checkFields() : public throws (void) +Method getNamespace() : public throws (java.lang.String) +Method setNamespace(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.GetRouteInfoRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.GetRouteInfoRequestHeader.schema new file mode 100644 index 0000000..c4b351c --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.GetRouteInfoRequestHeader.schema @@ -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. + */ + + +Field acceptStandardJsonOnly : private java.lang.Boolean null +Field topic : private java.lang.String null +Method checkFields() : public throws (void) +Method getAcceptStandardJsonOnly() : public throws (java.lang.Boolean) +Method getTopic() : public throws (java.lang.String) +Method setAcceptStandardJsonOnly(java.lang.Boolean) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.PutKVConfigRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.PutKVConfigRequestHeader.schema new file mode 100644 index 0000000..ac28946 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.PutKVConfigRequestHeader.schema @@ -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. + */ + + +Field key : private java.lang.String null +Field namespace : private java.lang.String null +Field value : private java.lang.String null +Method checkFields() : public throws (void) +Method getKey() : public throws (java.lang.String) +Method getNamespace() : public throws (java.lang.String) +Method getValue() : public throws (java.lang.String) +Method setKey(java.lang.String) : public throws (void) +Method setNamespace(java.lang.String) : public throws (void) +Method setValue(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.QueryDataVersionRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.QueryDataVersionRequestHeader.schema new file mode 100644 index 0000000..c7477bd --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.QueryDataVersionRequestHeader.schema @@ -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. + */ + + +Field brokerAddr : private java.lang.String null +Field brokerId : private java.lang.Long null +Field brokerName : private java.lang.String null +Field clusterName : private java.lang.String null +Method checkFields() : public throws (void) +Method getBrokerAddr() : public throws (java.lang.String) +Method getBrokerId() : public throws (java.lang.Long) +Method getBrokerName() : public throws (java.lang.String) +Method getClusterName() : public throws (java.lang.String) +Method setBrokerAddr(java.lang.String) : public throws (void) +Method setBrokerId(java.lang.Long) : public throws (void) +Method setBrokerName(java.lang.String) : public throws (void) +Method setClusterName(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.QueryDataVersionResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.QueryDataVersionResponseHeader.schema new file mode 100644 index 0000000..f0b14bf --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.QueryDataVersionResponseHeader.schema @@ -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. + */ + + +Field changed : private java.lang.Boolean null +Method checkFields() : public throws (void) +Method getChanged() : public throws (java.lang.Boolean) +Method setChanged(java.lang.Boolean) : public throws (void) +Method toString() : public throws (java.lang.String) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.RegisterBrokerRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.RegisterBrokerRequestHeader.schema new file mode 100644 index 0000000..5d8f5ae --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.RegisterBrokerRequestHeader.schema @@ -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. + */ + + +Field bodyCrc32 : private java.lang.Integer 0 +Field brokerAddr : private java.lang.String null +Field brokerId : private java.lang.Long null +Field brokerName : private java.lang.String null +Field clusterName : private java.lang.String null +Field compressed : private boolean false +Field haServerAddr : private java.lang.String null +Method checkFields() : public throws (void) +Method getBodyCrc32() : public throws (java.lang.Integer) +Method getBrokerAddr() : public throws (java.lang.String) +Method getBrokerId() : public throws (java.lang.Long) +Method getBrokerName() : public throws (java.lang.String) +Method getClusterName() : public throws (java.lang.String) +Method getHaServerAddr() : public throws (java.lang.String) +Method isCompressed() : public throws (boolean) +Method setBodyCrc32(java.lang.Integer) : public throws (void) +Method setBrokerAddr(java.lang.String) : public throws (void) +Method setBrokerId(java.lang.Long) : public throws (void) +Method setBrokerName(java.lang.String) : public throws (void) +Method setClusterName(java.lang.String) : public throws (void) +Method setCompressed(boolean) : public throws (void) +Method setHaServerAddr(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.RegisterBrokerResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.RegisterBrokerResponseHeader.schema new file mode 100644 index 0000000..be2a8f1 --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.RegisterBrokerResponseHeader.schema @@ -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. + */ + + +Field haServerAddr : private java.lang.String null +Field masterAddr : private java.lang.String null +Method checkFields() : public throws (void) +Method getHaServerAddr() : public throws (java.lang.String) +Method getMasterAddr() : public throws (java.lang.String) +Method setHaServerAddr(java.lang.String) : public throws (void) +Method setMasterAddr(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.RegisterOrderTopicRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.RegisterOrderTopicRequestHeader.schema new file mode 100644 index 0000000..e60b7cd --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.RegisterOrderTopicRequestHeader.schema @@ -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. + */ + + +Field orderTopicString : private java.lang.String null +Field topic : private java.lang.String null +Method checkFields() : public throws (void) +Method getOrderTopicString() : public throws (java.lang.String) +Method getTopic() : public throws (java.lang.String) +Method setOrderTopicString(java.lang.String) : public throws (void) +Method setTopic(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.UnRegisterBrokerRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.UnRegisterBrokerRequestHeader.schema new file mode 100644 index 0000000..c7477bd --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.UnRegisterBrokerRequestHeader.schema @@ -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. + */ + + +Field brokerAddr : private java.lang.String null +Field brokerId : private java.lang.Long null +Field brokerName : private java.lang.String null +Field clusterName : private java.lang.String null +Method checkFields() : public throws (void) +Method getBrokerAddr() : public throws (java.lang.String) +Method getBrokerId() : public throws (java.lang.Long) +Method getBrokerName() : public throws (java.lang.String) +Method getClusterName() : public throws (java.lang.String) +Method setBrokerAddr(java.lang.String) : public throws (void) +Method setBrokerId(java.lang.Long) : public throws (void) +Method setBrokerName(java.lang.String) : public throws (void) +Method setClusterName(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.WipeWritePermOfBrokerRequestHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.WipeWritePermOfBrokerRequestHeader.schema new file mode 100644 index 0000000..6fd2eae --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.WipeWritePermOfBrokerRequestHeader.schema @@ -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. + */ + + +Field brokerName : private java.lang.String null +Method checkFields() : public throws (void) +Method getBrokerName() : public throws (java.lang.String) +Method setBrokerName(java.lang.String) : public throws (void) diff --git a/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.WipeWritePermOfBrokerResponseHeader.schema b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.WipeWritePermOfBrokerResponseHeader.schema new file mode 100644 index 0000000..2db91ea --- /dev/null +++ b/test/src/test/resources/schema/protocol/common.protocol.header.namesrv.WipeWritePermOfBrokerResponseHeader.schema @@ -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. + */ + + +Field wipeTopicCount : private java.lang.Integer null +Method checkFields() : public throws (void) +Method getWipeTopicCount() : public throws (java.lang.Integer) +Method setWipeTopicCount(java.lang.Integer) : public throws (void) diff --git a/tieredstore/BUILD.bazel b/tieredstore/BUILD.bazel new file mode 100644 index 0000000..8822280 --- /dev/null +++ b/tieredstore/BUILD.bazel @@ -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. +# +load("//bazel:GenTestRules.bzl", "GenTestRules") + +java_library( + name = "tieredstore", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//common", + "//remoting", + "//store", + "@maven//:com_github_ben_manes_caffeine_caffeine", + "@maven//:com_google_code_findbugs_jsr305", + "@maven//:com_google_guava_guava", + "@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_otlp", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:org_apache_tomcat_annotations_api", + "@maven//:com_alibaba_fastjson", + "@maven//:org_apache_rocketmq_rocketmq_rocksdb", + "@maven//:commons_collections_commons_collections", + "@maven//:org_slf4j_slf4j_api", + ], +) + +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 = [ + ":tieredstore", + "//:test_deps", + "//common", + "//remoting", + "//store", + "@maven//:com_alibaba_fastjson", + "@maven//:commons_io_commons_io", + "@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", + "@maven//:com_google_guava_guava", + "@maven//:io_github_aliyunmq_rocketmq_slf4j_api", + "@maven//:io_github_aliyunmq_rocketmq_logback_classic", + "@maven//:net_java_dev_jna_jna", + "@maven//:org_slf4j_slf4j_api", + ], +) + +GenTestRules( + name = "GeneratedTestRules", + exclude_tests = [ + ], + medium_tests = [ + ], + test_files = glob(["src/test/java/**/*Test.java"]), + deps = [ + ":tests", + ], +) diff --git a/tieredstore/README.md b/tieredstore/README.md new file mode 100644 index 0000000..41e7458 --- /dev/null +++ b/tieredstore/README.md @@ -0,0 +1,64 @@ +# Tiered storage for RocketMQ (Technical preview) + +RocketMQ tiered storage allows users to offload message data from the local disk to other cheaper and larger storage mediums. So that users can extend the message reserve time at a lower cost. And different topics can flexibly specify different TTL as needed. + +This article is a cookbook for RocketMQ tiered storage. + +## Architecture + +![Tiered storage architecture](tiered_storage_arch.png) + +## Quick start + +Use the following steps to easily use tiered storage + +1. Change `messageStorePlugIn` to `org.apache.rocketmq.tieredstore.TieredMessageStore` in your `broker.conf`. +2. Configure your backend service provider. change `tieredBackendServiceProvider` to your storage medium implement. We give a default implement: POSIX provider, and you need to change `tieredStoreFilePath` to the mount point of storage medium for tiered storage. +3. Start the broker and enjoy! + +## Configuration + +The following are some core configurations, for more details, see [TieredMessageStoreConfig](https://github.com/apache/rocketmq/blob/develop/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/common/TieredMessageStoreConfig.java) + +| Configuration | Default value | Unit | Function | +| ------------------------------- |---------------------------------------------------------------| ----------- | ------------------------------------------------------------------------------- | +| messageStorePlugIn | | | Set to org.apache.rocketmq.tieredstore.TieredMessageStore to use tiered storage | +| tieredMetadataServiceProvider | org.apache.rocketmq.tieredstore.metadata.DefaultMetadataStore | | Select your metadata provider | +| tieredBackendServiceProvider | org.apache.rocketmq.tieredstore.provider.PosixFileSegment | | Select your backend service provider | +| tieredStoreFilePath | | | Select the directory using for tiered storage, only for POSIX provider. | +| tieredStorageLevel | NOT_IN_DISK | | The options are DISABLE, NOT_IN_DISK, NOT_IN_MEM, FORCE | +| tieredStoreFileReservedTime | 72 | hour | Default topic TTL in tiered storage | +| tieredStoreGroupCommitCount | 2500 | | The number of messages that trigger one batch transfer | +| tieredStoreGroupCommitSize | 33554432 | byte | The size of messages that trigger one batch transfer, 32M by default | +| tieredStoreMaxGroupCommitCount | 10000 | | The maximum number of messages waiting to be transfered per queue | +| readAheadCacheExpireDuration | 1000 | millisecond | Read-ahead cache expiration time | +| readAheadCacheSizeThresholdRate | 0.3 | | The maximum heap space occupied by the read-ahead cache | + +## Metrics + +Tiered storage provides some useful metrics, see [RIP-46](https://github.com/apache/rocketmq/wiki/RIP-46-Observability-improvement-for-RocketMQ) for details. + +| Type | Name | Unit | +| --------- | --------------------------------------------------- | ------------ | +| Histogram | rocketmq_tiered_store_api_latency | milliseconds | +| Histogram | rocketmq_tiered_store_provider_rpc_latency | milliseconds | +| Histogram | rocketmq_tiered_store_provider_upload_bytes | byte | +| Histogram | rocketmq_tiered_store_provider_download_bytes | byte | +| Gauge | rocketmq_tiered_store_dispatch_behind | | +| Gauge | rocketmq_tiered_store_dispatch_latency | byte | +| Counter | rocketmq_tiered_store_messages_dispatch_total | | +| Counter | rocketmq_tiered_store_messages_out_total | | +| Counter | rocketmq_tiered_store_get_message_fallback_total | | +| Gauge | rocketmq_tiered_store_read_ahead_cache_count | | +| Gauge | rocketmq_tiered_store_read_ahead_cache_bytes | byte | +| Counter | rocketmq_tiered_store_read_ahead_cache_access_total | | +| Counter | rocketmq_tiered_store_read_ahead_cache_hit_total | | +| Gauge | rocketmq_storage_message_reserve_time | milliseconds | + +## How to contribute + +We need community participation to add more backend service providers for tiered storage. [PosixFileSegment](https://github.com/apache/rocketmq/blob/develop/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/provider/PosixFileSegment.java), the implementation provided by default is just an example. People who want to contribute can follow it to implement their own providers, such as S3FileSegment, OSSFileSegment, and MinIOFileSegment. Here are some guidelines: + +1. Extend [FileSegment](https://github.com/apache/rocketmq/blob/develop/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/provider/FileSegment.java) and implement the methods of [FileSegmentProvider](https://github.com/apache/rocketmq/blob/develop/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/provider/FileSegmentProvider.java) interface. +2. Record metrics where appropriate. See `rocketmq_tiered_store_provider_rpc_latency`, `rocketmq_tiered_store_provider_upload_bytes`, and `rocketmq_tiered_store_provider_download_bytes` +3. No need to maintain your own cache and avoid polluting the page cache. It is already having the read-ahead cache. diff --git a/tieredstore/pom.xml b/tieredstore/pom.xml new file mode 100644 index 0000000..691e240 --- /dev/null +++ b/tieredstore/pom.xml @@ -0,0 +1,71 @@ + + + + + org.apache.rocketmq + rocketmq-all + 5.3.4-SNAPSHOT + + + 4.0.0 + jar + rocketmq-tiered-store + rocketmq-tiered-store ${project.version} + + + ${basedir}/.. + + + + + org.apache.rocketmq + rocketmq-store + + + + com.github.ben-manes.caffeine + caffeine + + + org.checkerframework + checker-qual + + + + + + commons-io + commons-io + test + + + + org.openjdk.jmh + jmh-core + 1.36 + provided + + + + org.openjdk.jmh + jmh-generator-annprocess + 1.36 + provided + + + diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/MessageStoreConfig.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/MessageStoreConfig.java new file mode 100644 index 0000000..1066756 --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/MessageStoreConfig.java @@ -0,0 +1,421 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tieredstore; + +import java.io.File; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.time.Duration; + +public class MessageStoreConfig { + + private String brokerName = localHostName(); + private String brokerClusterName = "DefaultCluster"; + private TieredStorageLevel tieredStorageLevel = TieredStorageLevel.NOT_IN_DISK; + + /** + * All fetch requests are judged against this level first, + * and if the message cannot be read from the TiredMessageStore, + * these requests will still go to the next store for fallback processing. + */ + public enum TieredStorageLevel { + /** + * Disable tiered storage, all fetch request will be handled by default message store. + */ + DISABLE(0), + /** + * Only fetch request with offset not in disk will be handled by tiered storage. + */ + NOT_IN_DISK(1), + /** + * Only fetch request with offset not in memory(page cache) will be handled by tiered storage. + */ + NOT_IN_MEM(2), + /** + * All fetch request will be handled by tiered storage. + */ + FORCE(3); + + private final int value; + + TieredStorageLevel(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + @SuppressWarnings("DuplicatedCode") + public static TieredStorageLevel valueOf(int value) { + switch (value) { + case 1: + return NOT_IN_DISK; + case 2: + return NOT_IN_MEM; + case 3: + return FORCE; + default: + return DISABLE; + } + } + + public boolean isEnable() { + return this.value > 0; + } + + public boolean check(TieredStorageLevel targetLevel) { + return this.value >= targetLevel.value; + } + } + + private String storePathRootDir = System.getProperty("user.home") + File.separator + "store"; + private boolean messageIndexEnable = true; + private boolean recordGetMessageResult = false; + + // CommitLog file size, default is 1G + private long tieredStoreCommitLogMaxSize = 1024 * 1024 * 1024; + // ConsumeQueue file size, default is 100M + private long tieredStoreConsumeQueueMaxSize = 100 * 1024 * 1024; + private int tieredStoreIndexFileMaxHashSlotNum = 5000000; + private int tieredStoreIndexFileMaxIndexNum = 5000000 * 4; + + private String tieredMetadataServiceProvider = "org.apache.rocketmq.tieredstore.metadata.DefaultMetadataStore"; + private String tieredBackendServiceProvider = "org.apache.rocketmq.tieredstore.provider.MemoryFileSegment"; + + // file reserved time, default is 72 hour + private boolean tieredStoreDeleteFileEnable = true; + private int tieredStoreFileReservedTime = 72; + private long tieredStoreDeleteFileInterval = Duration.ofHours(1).toMillis(); + + // time of forcing commitLog to roll to next file, default is 24 hour + private int commitLogRollingInterval = 24; + private int commitLogRollingMinimumSize = 16 * 1024 * 1024; + + private boolean tieredStoreGroupCommit = true; + private int tieredStoreGroupCommitTimeout = 30 * 1000; + // Cached message count larger than this value will trigger async commit. default is 4096 + private int tieredStoreGroupCommitCount = 4 * 1024; + // Cached message size larger than this value will trigger async commit. default is 4M + private int tieredStoreGroupCommitSize = 4 * 1024 * 1024; + // Cached message count larger than this value will suspend append. default is 10000 + private int tieredStoreMaxGroupCommitCount = 10000; + + private boolean readAheadCacheEnable = true; + private int readAheadMessageCountThreshold = 4096; + private int readAheadMessageSizeThreshold = 16 * 1024 * 1024; + private long readAheadCacheExpireDuration = 15 * 1000; + private double readAheadCacheSizeThresholdRate = 0.3; + + private int tieredStoreMaxPendingLimit = 10000; + private boolean tieredStoreCrcCheckEnable = false; + + private String tieredStoreFilePath = ""; + private String objectStoreEndpoint = ""; + private String objectStoreBucket = ""; + private String objectStoreAccessKey = ""; + private String objectStoreSecretKey = ""; + + public static String localHostName() { + try { + return InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException ignore) { + } + + return "DEFAULT_BROKER"; + } + + 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 TieredStorageLevel getTieredStorageLevel() { + return tieredStorageLevel; + } + + public void setTieredStorageLevel(TieredStorageLevel tieredStorageLevel) { + this.tieredStorageLevel = tieredStorageLevel; + } + + public void setTieredStorageLevel(int tieredStorageLevel) { + this.tieredStorageLevel = TieredStorageLevel.valueOf(tieredStorageLevel); + } + + public void setTieredStorageLevel(String tieredStorageLevel) { + this.tieredStorageLevel = TieredStorageLevel.valueOf(tieredStorageLevel); + } + + public String getStorePathRootDir() { + return storePathRootDir; + } + + public void setStorePathRootDir(String storePathRootDir) { + this.storePathRootDir = storePathRootDir; + } + + public boolean isMessageIndexEnable() { + return messageIndexEnable; + } + + public void setMessageIndexEnable(boolean messageIndexEnable) { + this.messageIndexEnable = messageIndexEnable; + } + + public boolean isRecordGetMessageResult() { + return recordGetMessageResult; + } + + public void setRecordGetMessageResult(boolean recordGetMessageResult) { + this.recordGetMessageResult = recordGetMessageResult; + } + + public long getTieredStoreCommitLogMaxSize() { + return tieredStoreCommitLogMaxSize; + } + + public void setTieredStoreCommitLogMaxSize(long tieredStoreCommitLogMaxSize) { + this.tieredStoreCommitLogMaxSize = tieredStoreCommitLogMaxSize; + } + + public long getTieredStoreConsumeQueueMaxSize() { + return tieredStoreConsumeQueueMaxSize; + } + + public void setTieredStoreConsumeQueueMaxSize(long tieredStoreConsumeQueueMaxSize) { + this.tieredStoreConsumeQueueMaxSize = tieredStoreConsumeQueueMaxSize; + } + + public int getTieredStoreIndexFileMaxHashSlotNum() { + return tieredStoreIndexFileMaxHashSlotNum; + } + + public void setTieredStoreIndexFileMaxHashSlotNum(int tieredStoreIndexFileMaxHashSlotNum) { + this.tieredStoreIndexFileMaxHashSlotNum = tieredStoreIndexFileMaxHashSlotNum; + } + + public int getTieredStoreIndexFileMaxIndexNum() { + return tieredStoreIndexFileMaxIndexNum; + } + + public void setTieredStoreIndexFileMaxIndexNum(int tieredStoreIndexFileMaxIndexNum) { + this.tieredStoreIndexFileMaxIndexNum = tieredStoreIndexFileMaxIndexNum; + } + + public String getTieredMetadataServiceProvider() { + return tieredMetadataServiceProvider; + } + + public void setTieredMetadataServiceProvider(String tieredMetadataServiceProvider) { + this.tieredMetadataServiceProvider = tieredMetadataServiceProvider; + } + + public String getTieredBackendServiceProvider() { + return tieredBackendServiceProvider; + } + + public void setTieredBackendServiceProvider(String tieredBackendServiceProvider) { + this.tieredBackendServiceProvider = tieredBackendServiceProvider; + } + + public boolean isTieredStoreDeleteFileEnable() { + return tieredStoreDeleteFileEnable; + } + + public void setTieredStoreDeleteFileEnable(boolean tieredStoreDeleteFileEnable) { + this.tieredStoreDeleteFileEnable = tieredStoreDeleteFileEnable; + } + + public int getTieredStoreFileReservedTime() { + return tieredStoreFileReservedTime; + } + + public void setTieredStoreFileReservedTime(int tieredStoreFileReservedTime) { + this.tieredStoreFileReservedTime = tieredStoreFileReservedTime; + } + + public long getTieredStoreDeleteFileInterval() { + return tieredStoreDeleteFileInterval; + } + + public void setTieredStoreDeleteFileInterval(long tieredStoreDeleteFileInterval) { + this.tieredStoreDeleteFileInterval = tieredStoreDeleteFileInterval; + } + + public int getCommitLogRollingInterval() { + return commitLogRollingInterval; + } + + public void setCommitLogRollingInterval(int commitLogRollingInterval) { + this.commitLogRollingInterval = commitLogRollingInterval; + } + + public int getCommitLogRollingMinimumSize() { + return commitLogRollingMinimumSize; + } + + public void setCommitLogRollingMinimumSize(int commitLogRollingMinimumSize) { + this.commitLogRollingMinimumSize = commitLogRollingMinimumSize; + } + + public boolean isTieredStoreGroupCommit() { + return tieredStoreGroupCommit; + } + + public void setTieredStoreGroupCommit(boolean tieredStoreGroupCommit) { + this.tieredStoreGroupCommit = tieredStoreGroupCommit; + } + + public int getTieredStoreGroupCommitTimeout() { + return tieredStoreGroupCommitTimeout; + } + + public void setTieredStoreGroupCommitTimeout(int tieredStoreGroupCommitTimeout) { + this.tieredStoreGroupCommitTimeout = tieredStoreGroupCommitTimeout; + } + + public int getTieredStoreGroupCommitCount() { + return tieredStoreGroupCommitCount; + } + + public void setTieredStoreGroupCommitCount(int tieredStoreGroupCommitCount) { + this.tieredStoreGroupCommitCount = tieredStoreGroupCommitCount; + } + + public int getTieredStoreGroupCommitSize() { + return tieredStoreGroupCommitSize; + } + + public void setTieredStoreGroupCommitSize(int tieredStoreGroupCommitSize) { + this.tieredStoreGroupCommitSize = tieredStoreGroupCommitSize; + } + + public int getTieredStoreMaxGroupCommitCount() { + return tieredStoreMaxGroupCommitCount; + } + + public void setTieredStoreMaxGroupCommitCount(int tieredStoreMaxGroupCommitCount) { + this.tieredStoreMaxGroupCommitCount = tieredStoreMaxGroupCommitCount; + } + + public boolean isReadAheadCacheEnable() { + return readAheadCacheEnable; + } + + public void setReadAheadCacheEnable(boolean readAheadCacheEnable) { + this.readAheadCacheEnable = readAheadCacheEnable; + } + + public int getReadAheadMessageCountThreshold() { + return readAheadMessageCountThreshold; + } + + public void setReadAheadMessageCountThreshold(int readAheadMessageCountThreshold) { + this.readAheadMessageCountThreshold = readAheadMessageCountThreshold; + } + + public int getReadAheadMessageSizeThreshold() { + return readAheadMessageSizeThreshold; + } + + public void setReadAheadMessageSizeThreshold(int readAheadMessageSizeThreshold) { + this.readAheadMessageSizeThreshold = readAheadMessageSizeThreshold; + } + + public long getReadAheadCacheExpireDuration() { + return readAheadCacheExpireDuration; + } + + public void setReadAheadCacheExpireDuration(long duration) { + this.readAheadCacheExpireDuration = duration; + } + + public double getReadAheadCacheSizeThresholdRate() { + return readAheadCacheSizeThresholdRate; + } + + public void setReadAheadCacheSizeThresholdRate(double rate) { + this.readAheadCacheSizeThresholdRate = rate; + } + + public int getTieredStoreMaxPendingLimit() { + return tieredStoreMaxPendingLimit; + } + + public void setTieredStoreMaxPendingLimit(int tieredStoreMaxPendingLimit) { + this.tieredStoreMaxPendingLimit = tieredStoreMaxPendingLimit; + } + + public boolean isTieredStoreCrcCheckEnable() { + return tieredStoreCrcCheckEnable; + } + + public void setTieredStoreCrcCheckEnable(boolean tieredStoreCrcCheckEnable) { + this.tieredStoreCrcCheckEnable = tieredStoreCrcCheckEnable; + } + + public String getTieredStoreFilePath() { + return tieredStoreFilePath; + } + + public void setTieredStoreFilePath(String tieredStoreFilePath) { + this.tieredStoreFilePath = tieredStoreFilePath; + } + + public void setObjectStoreEndpoint(String objectStoreEndpoint) { + this.objectStoreEndpoint = objectStoreEndpoint; + } + + public String getObjectStoreBucket() { + return objectStoreBucket; + } + + public void setObjectStoreBucket(String objectStoreBucket) { + this.objectStoreBucket = objectStoreBucket; + } + + public String getObjectStoreAccessKey() { + return objectStoreAccessKey; + } + + public void setObjectStoreAccessKey(String objectStoreAccessKey) { + this.objectStoreAccessKey = objectStoreAccessKey; + } + + public String getObjectStoreSecretKey() { + return objectStoreSecretKey; + } + + public void setObjectStoreSecretKey(String objectStoreSecretKey) { + this.objectStoreSecretKey = objectStoreSecretKey; + } + + public String getObjectStoreEndpoint() { + return objectStoreEndpoint; + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/MessageStoreExecutor.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/MessageStoreExecutor.java new file mode 100644 index 0000000..56f564e --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/MessageStoreExecutor.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.tieredstore; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.utils.ThreadUtils; + +public class MessageStoreExecutor { + + public final BlockingQueue bufferCommitThreadPoolQueue; + public final BlockingQueue bufferFetchThreadPoolQueue; + public final BlockingQueue fileRecyclingThreadPoolQueue; + + public final ScheduledExecutorService commonExecutor; + public final ExecutorService bufferCommitExecutor; + public final ExecutorService bufferFetchExecutor; + public final ExecutorService fileRecyclingExecutor; + + private static class SingletonHolder { + private static final MessageStoreExecutor INSTANCE = new MessageStoreExecutor(); + } + + public static MessageStoreExecutor getInstance() { + return SingletonHolder.INSTANCE; + } + + public MessageStoreExecutor() { + this(10000); + } + + public MessageStoreExecutor(int maxQueueCapacity) { + + this.commonExecutor = ThreadUtils.newScheduledThreadPool( + Math.max(4, Runtime.getRuntime().availableProcessors()), + new ThreadFactoryImpl("TieredCommonExecutor_")); + + this.bufferCommitThreadPoolQueue = new LinkedBlockingQueue<>(maxQueueCapacity); + this.bufferCommitExecutor = ThreadUtils.newThreadPoolExecutor( + Math.max(16, Runtime.getRuntime().availableProcessors() * 4), + Math.max(16, Runtime.getRuntime().availableProcessors() * 4), + TimeUnit.MINUTES.toMillis(1), TimeUnit.MILLISECONDS, + this.bufferCommitThreadPoolQueue, + new ThreadFactoryImpl("BufferCommitExecutor_")); + + this.bufferFetchThreadPoolQueue = new LinkedBlockingQueue<>(maxQueueCapacity); + this.bufferFetchExecutor = ThreadUtils.newThreadPoolExecutor( + Math.max(16, Runtime.getRuntime().availableProcessors() * 4), + Math.max(16, Runtime.getRuntime().availableProcessors() * 4), + TimeUnit.MINUTES.toMillis(1), TimeUnit.MILLISECONDS, + this.bufferFetchThreadPoolQueue, + new ThreadFactoryImpl("BufferFetchExecutor_")); + + this.fileRecyclingThreadPoolQueue = new LinkedBlockingQueue<>(maxQueueCapacity); + this.fileRecyclingExecutor = ThreadUtils.newThreadPoolExecutor( + Math.max(4, Runtime.getRuntime().availableProcessors()), + Math.max(4, Runtime.getRuntime().availableProcessors()), + TimeUnit.MINUTES.toMillis(1), TimeUnit.MILLISECONDS, + this.fileRecyclingThreadPoolQueue, + new ThreadFactoryImpl("BufferFetchExecutor_")); + } + + private void shutdownExecutor(ExecutorService executor) { + if (executor != null) { + executor.shutdown(); + } + } + + public void shutdown() { + this.shutdownExecutor(this.commonExecutor); + this.shutdownExecutor(this.bufferCommitExecutor); + this.shutdownExecutor(this.bufferFetchExecutor); + this.shutdownExecutor(this.fileRecyclingExecutor); + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/TieredMessageStore.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/TieredMessageStore.java new file mode 100644 index 0000000..f1c935d --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/TieredMessageStore.java @@ -0,0 +1,493 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tieredstore; + +import com.google.common.base.Stopwatch; +import com.google.common.collect.Sets; +import io.opentelemetry.api.common.Attributes; +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.lang.reflect.Constructor; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.topic.TopicValidator; +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.QueryMessageResult; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.plugin.AbstractPluginMessageStore; +import org.apache.rocketmq.store.plugin.MessageStorePluginContext; +import org.apache.rocketmq.tieredstore.core.MessageStoreDispatcher; +import org.apache.rocketmq.tieredstore.core.MessageStoreDispatcherImpl; +import org.apache.rocketmq.tieredstore.core.MessageStoreFetcher; +import org.apache.rocketmq.tieredstore.core.MessageStoreFetcherImpl; +import org.apache.rocketmq.tieredstore.core.MessageStoreFilter; +import org.apache.rocketmq.tieredstore.core.MessageStoreTopicFilter; +import org.apache.rocketmq.tieredstore.file.FlatFileStore; +import org.apache.rocketmq.tieredstore.file.FlatMessageFile; +import org.apache.rocketmq.tieredstore.index.IndexService; +import org.apache.rocketmq.tieredstore.index.IndexStoreService; +import org.apache.rocketmq.tieredstore.metadata.MetadataStore; +import org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsConstant; +import org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsManager; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TieredMessageStore extends AbstractPluginMessageStore { + + protected static final Logger log = LoggerFactory.getLogger(MessageStoreUtil.TIERED_STORE_LOGGER_NAME); + protected static final long MIN_STORE_TIME = -1L; + + protected final String brokerName; + protected final MessageStore defaultStore; + protected final MessageStoreConfig storeConfig; + protected final MessageStorePluginContext context; + + protected final MetadataStore metadataStore; + protected final MessageStoreExecutor storeExecutor; + protected final IndexService indexService; + protected final FlatFileStore flatFileStore; + protected final MessageStoreFilter topicFilter; + protected final MessageStoreFetcher fetcher; + protected final MessageStoreDispatcher dispatcher; + + public TieredMessageStore(MessageStorePluginContext context, MessageStore next) { + super(context, next); + + this.storeConfig = new MessageStoreConfig(); + this.context = context; + this.context.registerConfiguration(this.storeConfig); + this.brokerName = this.storeConfig.getBrokerName(); + this.defaultStore = next; + + this.metadataStore = this.getMetadataStore(this.storeConfig); + this.topicFilter = new MessageStoreTopicFilter(this.storeConfig); + this.storeExecutor = new MessageStoreExecutor(); + this.flatFileStore = new FlatFileStore(this.storeConfig, this.metadataStore, this.storeExecutor); + this.indexService = new IndexStoreService(this.flatFileStore.getFlatFileFactory(), + MessageStoreUtil.getIndexFilePath(this.storeConfig.getBrokerName())); + this.fetcher = new MessageStoreFetcherImpl(this); + this.dispatcher = new MessageStoreDispatcherImpl(this); + next.addDispatcher(dispatcher); + } + + @Override + public boolean load() { + boolean loadFlatFile = flatFileStore.load(); + boolean loadNextStore = next.load(); + boolean result = loadFlatFile && loadNextStore; + if (result) { + indexService.start(); + dispatcher.start(); + storeExecutor.commonExecutor.scheduleWithFixedDelay( + flatFileStore::scheduleDeleteExpireFile, storeConfig.getTieredStoreDeleteFileInterval(), + storeConfig.getTieredStoreDeleteFileInterval(), TimeUnit.MILLISECONDS); + } + return result; + } + + public String getBrokerName() { + return brokerName; + } + + public MessageStoreConfig getStoreConfig() { + return storeConfig; + } + + public MessageStore getDefaultStore() { + return defaultStore; + } + + private MetadataStore getMetadataStore(MessageStoreConfig storeConfig) { + try { + Class clazz = + Class.forName(storeConfig.getTieredMetadataServiceProvider()).asSubclass(MetadataStore.class); + Constructor constructor = clazz.getConstructor(MessageStoreConfig.class); + return constructor.newInstance(storeConfig); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public MetadataStore getMetadataStore() { + return metadataStore; + } + + public MessageStoreFilter getTopicFilter() { + return topicFilter; + } + + public MessageStoreExecutor getStoreExecutor() { + return storeExecutor; + } + + public FlatFileStore getFlatFileStore() { + return flatFileStore; + } + + public IndexService getIndexService() { + return indexService; + } + + public boolean fetchFromCurrentStore(String topic, int queueId, long offset) { + return fetchFromCurrentStore(topic, queueId, offset, 1); + } + + @SuppressWarnings("all") + public boolean fetchFromCurrentStore(String topic, int queueId, long offset, int batchSize) { + MessageStoreConfig.TieredStorageLevel storageLevel = storeConfig.getTieredStorageLevel(); + + if (storageLevel.check(MessageStoreConfig.TieredStorageLevel.FORCE)) { + return true; + } + + if (!storageLevel.isEnable()) { + return false; + } + + FlatMessageFile flatFile = flatFileStore.getFlatFile(new MessageQueue(topic, brokerName, queueId)); + if (flatFile == null) { + return false; + } + + if (offset >= flatFile.getConsumeQueueCommitOffset()) { + return false; + } + + // determine whether tiered storage path conditions are met + if (storageLevel.check(MessageStoreConfig.TieredStorageLevel.NOT_IN_DISK)) { + // return true to read from tiered storage if the CommitLog is empty + if (next != null && next.getCommitLog() != null && + next.getCommitLog().getMinOffset() < 0L) { + return true; + } + if (!next.checkInStoreByConsumeOffset(topic, queueId, offset)) { + return true; + } + } + + if (storageLevel.check(MessageStoreConfig.TieredStorageLevel.NOT_IN_MEM) + && !next.checkInMemByConsumeOffset(topic, queueId, offset, batchSize)) { + return true; + } + return false; + } + + @Override + public GetMessageResult getMessage(String group, String topic, int queueId, long offset, int maxMsgNums, + MessageFilter messageFilter) { + return getMessageAsync(group, topic, queueId, offset, maxMsgNums, messageFilter).join(); + } + + @Override + public CompletableFuture getMessageAsync(String group, String topic, + int queueId, long offset, int maxMsgNums, MessageFilter messageFilter) { + + // for system topic, force reading from local store + if (topicFilter.filterTopic(topic)) { + return next.getMessageAsync(group, topic, queueId, offset, maxMsgNums, messageFilter); + } + + if (fetchFromCurrentStore(topic, queueId, offset, maxMsgNums)) { + log.trace("GetMessageAsync from remote store, " + + "topic: {}, queue: {}, offset: {}, maxCount: {}", topic, queueId, offset, maxMsgNums); + } else { + log.trace("GetMessageAsync from next store, " + + "topic: {}, queue: {}, offset: {}, maxCount: {}", topic, queueId, offset, maxMsgNums); + return next.getMessageAsync(group, topic, queueId, offset, maxMsgNums, messageFilter); + } + + Stopwatch stopwatch = Stopwatch.createStarted(); + return fetcher + .getMessageAsync(group, topic, queueId, offset, maxMsgNums, messageFilter) + .thenApply(result -> { + + Attributes latencyAttributes = TieredStoreMetricsManager.newAttributesBuilder() + .put(TieredStoreMetricsConstant.LABEL_OPERATION, TieredStoreMetricsConstant.OPERATION_API_GET_MESSAGE) + .put(TieredStoreMetricsConstant.LABEL_TOPIC, topic) + .put(TieredStoreMetricsConstant.LABEL_GROUP, group) + .build(); + TieredStoreMetricsManager.apiLatency.record(stopwatch.elapsed(TimeUnit.MILLISECONDS), latencyAttributes); + + if (result.getStatus() == GetMessageStatus.OFFSET_FOUND_NULL || + result.getStatus() == GetMessageStatus.NO_MATCHED_LOGIC_QUEUE) { + + if (next.checkInStoreByConsumeOffset(topic, queueId, offset)) { + TieredStoreMetricsManager.fallbackTotal.add(1, latencyAttributes); + log.debug("GetMessageAsync not found, then back to next store, result: {}, " + + "topic: {}, queue: {}, queue offset: {}, offset range: {}-{}", + result.getStatus(), topic, queueId, offset, result.getMinOffset(), result.getMaxOffset()); + return next.getMessage(group, topic, queueId, offset, maxMsgNums, messageFilter); + } + } + + if (result.getStatus() != GetMessageStatus.FOUND && + result.getStatus() != GetMessageStatus.NO_MESSAGE_IN_QUEUE && + result.getStatus() != GetMessageStatus.NO_MATCHED_LOGIC_QUEUE && + result.getStatus() != GetMessageStatus.OFFSET_TOO_SMALL && + result.getStatus() != GetMessageStatus.OFFSET_OVERFLOW_ONE && + result.getStatus() != GetMessageStatus.OFFSET_OVERFLOW_BADLY) { + log.warn("GetMessageAsync not found and message is not in next store, result: {}, " + + "topic: {}, queue: {}, queue offset: {}, offset range: {}-{}", + result.getStatus(), topic, queueId, offset, result.getMinOffset(), result.getMaxOffset()); + } + + if (result.getStatus() == GetMessageStatus.FOUND) { + Attributes messagesOutAttributes = TieredStoreMetricsManager.newAttributesBuilder() + .put(TieredStoreMetricsConstant.LABEL_TOPIC, topic) + .put(TieredStoreMetricsConstant.LABEL_GROUP, group) + .build(); + TieredStoreMetricsManager.messagesOutTotal.add(result.getMessageCount(), messagesOutAttributes); + + if (next.getStoreStatsService() != null) { + next.getStoreStatsService().getGetMessageTransferredMsgCount().add(result.getMessageCount()); + } + } + + // Fix min or max offset according next store at last + long minOffsetInQueue = next.getMinOffsetInQueue(topic, queueId); + if (minOffsetInQueue >= 0 && minOffsetInQueue < result.getMinOffset()) { + result.setMinOffset(minOffsetInQueue); + } + + // In general, the local cq offset is slightly greater than the commit offset in read message, + // so there is no need to update the maximum offset to the local cq offset here, + // otherwise it will cause repeated consumption after next start offset over commit offset. + + if (storeConfig.isRecordGetMessageResult()) { + log.info("GetMessageAsync result, {}, group: {}, topic: {}, queueId: {}, offset: {}, count:{}", + result, group, topic, queueId, offset, maxMsgNums); + } + + return result; + }).exceptionally(e -> { + log.error("GetMessageAsync from tiered store failed", e); + return next.getMessage(group, topic, queueId, offset, maxMsgNums, messageFilter); + }); + } + + @Override + public long getMinOffsetInQueue(String topic, int queueId) { + long minOffsetInNextStore = next.getMinOffsetInQueue(topic, queueId); + FlatMessageFile flatFile = flatFileStore.getFlatFile(new MessageQueue(topic, brokerName, queueId)); + if (flatFile == null) { + return minOffsetInNextStore; + } + long minOffsetInTieredStore = flatFile.getConsumeQueueMinOffset(); + if (minOffsetInTieredStore < 0) { + return minOffsetInNextStore; + } + return Math.min(minOffsetInNextStore, minOffsetInTieredStore); + } + + @Override + public long getEarliestMessageTime(String topic, int queueId) { + return getEarliestMessageTimeAsync(topic, queueId).join(); + } + + /** + * In the original design, getting the earliest time of the first message + * would generate two RPC requests. However, using the timestamp stored in the metadata + * avoids these requests, although this approach might introduce some level of inaccuracy. + */ + @Override + public CompletableFuture getEarliestMessageTimeAsync(String topic, int queueId) { + long localMinTime = next.getEarliestMessageTime(topic, queueId); + return fetcher.getEarliestMessageTimeAsync(topic, queueId) + .thenApply(remoteMinTime -> { + if (localMinTime > MIN_STORE_TIME && remoteMinTime > MIN_STORE_TIME) { + return Math.min(localMinTime, remoteMinTime); + } + return localMinTime > MIN_STORE_TIME ? localMinTime : + (remoteMinTime > MIN_STORE_TIME ? remoteMinTime : MIN_STORE_TIME); + }); + } + + @Override + public CompletableFuture getMessageStoreTimeStampAsync(String topic, int queueId, + long consumeQueueOffset) { + if (fetchFromCurrentStore(topic, queueId, consumeQueueOffset)) { + Stopwatch stopwatch = Stopwatch.createStarted(); + return fetcher.getMessageStoreTimeStampAsync(topic, queueId, consumeQueueOffset) + .thenApply(time -> { + Attributes latencyAttributes = TieredStoreMetricsManager.newAttributesBuilder() + .put(TieredStoreMetricsConstant.LABEL_OPERATION, + TieredStoreMetricsConstant.OPERATION_API_GET_TIME_BY_OFFSET) + .put(TieredStoreMetricsConstant.LABEL_TOPIC, topic) + .build(); + TieredStoreMetricsManager.apiLatency.record(stopwatch.elapsed(TimeUnit.MILLISECONDS), latencyAttributes); + if (time == -1) { + log.debug("GetEarliestMessageTimeAsync failed, try to get message time from next store, topic: {}, queue: {}, queue offset: {}", + topic, queueId, consumeQueueOffset); + return next.getMessageStoreTimeStamp(topic, queueId, consumeQueueOffset); + } + return time; + }); + } + return next.getMessageStoreTimeStampAsync(topic, queueId, consumeQueueOffset); + } + + @Override + public long getOffsetInQueueByTime(String topic, int queueId, long timestamp) { + return getOffsetInQueueByTime(topic, queueId, timestamp, BoundaryType.LOWER); + } + + @Override + public long getOffsetInQueueByTime(String topic, int queueId, long timestamp, BoundaryType boundaryType) { + boolean isForce = storeConfig.getTieredStorageLevel() == MessageStoreConfig.TieredStorageLevel.FORCE; + if (timestamp < next.getEarliestMessageTime() || isForce) { + Stopwatch stopwatch = Stopwatch.createStarted(); + long offsetInTieredStore = fetcher.getOffsetInQueueByTime(topic, queueId, timestamp, boundaryType); + Attributes latencyAttributes = TieredStoreMetricsManager.newAttributesBuilder() + .put(TieredStoreMetricsConstant.LABEL_OPERATION, TieredStoreMetricsConstant.OPERATION_API_GET_OFFSET_BY_TIME) + .put(TieredStoreMetricsConstant.LABEL_TOPIC, topic) + .build(); + TieredStoreMetricsManager.apiLatency.record(stopwatch.elapsed(TimeUnit.MILLISECONDS), latencyAttributes); + if (offsetInTieredStore == -1L && !isForce) { + return next.getOffsetInQueueByTime(topic, queueId, timestamp); + } + return offsetInTieredStore; + } + return next.getOffsetInQueueByTime(topic, queueId, timestamp); + } + + @Override + public QueryMessageResult queryMessage(String topic, String key, int maxNum, long begin, long end) { + return queryMessageAsync(topic, key, maxNum, begin, end).join(); + } + + @Override + public CompletableFuture queryMessageAsync(String topic, String key, + int maxNum, long begin, long end) { + long earliestTimeInNextStore = next.getEarliestMessageTime(); + if (earliestTimeInNextStore <= 0) { + log.warn("TieredMessageStore#queryMessageAsync: get earliest message time in next store failed: {}", earliestTimeInNextStore); + } + boolean isForce = storeConfig.getTieredStorageLevel() == MessageStoreConfig.TieredStorageLevel.FORCE; + QueryMessageResult result = end < earliestTimeInNextStore || isForce ? + new QueryMessageResult() : + next.queryMessage(topic, key, maxNum, begin, end); + int resultSize = result.getMessageBufferList().size(); + if (resultSize < maxNum && begin < earliestTimeInNextStore || isForce) { + Stopwatch stopwatch = Stopwatch.createStarted(); + try { + return fetcher.queryMessageAsync(topic, key, maxNum - resultSize, begin, isForce ? end : earliestTimeInNextStore) + .thenApply(tieredStoreResult -> { + Attributes latencyAttributes = TieredStoreMetricsManager.newAttributesBuilder() + .put(TieredStoreMetricsConstant.LABEL_OPERATION, TieredStoreMetricsConstant.OPERATION_API_QUERY_MESSAGE) + .put(TieredStoreMetricsConstant.LABEL_TOPIC, topic) + .build(); + TieredStoreMetricsManager.apiLatency.record(stopwatch.elapsed(TimeUnit.MILLISECONDS), latencyAttributes); + for (SelectMappedBufferResult msg : tieredStoreResult.getMessageMapedList()) { + result.addMessage(msg); + } + return result; + }); + } catch (Exception e) { + log.error("TieredMessageStore#queryMessageAsync: query message in tiered store failed", e); + return CompletableFuture.completedFuture(result); + } + } + return CompletableFuture.completedFuture(result); + } + + @Override + public List> getMetricsView() { + List> res = super.getMetricsView(); + res.addAll(TieredStoreMetricsManager.getMetricsView()); + return res; + } + + @Override + public void initMetrics(Meter meter, Supplier attributesBuilderSupplier) { + super.initMetrics(meter, attributesBuilderSupplier); + TieredStoreMetricsManager.init(meter, attributesBuilderSupplier, storeConfig, fetcher, flatFileStore, next); + } + + @Override + public int cleanUnusedTopic(Set retainTopics) { + metadataStore.iterateTopic(topicMetadata -> { + String topic = topicMetadata.getTopic(); + if (retainTopics.contains(topic) || + TopicValidator.isSystemTopic(topic) || + MixAll.isLmq(topic)) { + return; + } + this.deleteTopics(Sets.newHashSet(topicMetadata.getTopic())); + }); + return next.cleanUnusedTopic(retainTopics); + } + + @Override + public int deleteTopics(Set deleteTopics) { + for (String topic : deleteTopics) { + metadataStore.iterateQueue(topic, queueMetadata -> { + flatFileStore.destroyFile(queueMetadata.getQueue()); + }); + metadataStore.deleteTopic(topic); + log.info("MessageStore delete topic success, topicName={}", topic); + } + return next.deleteTopics(deleteTopics); + } + + @Override + public synchronized void shutdown() { + if (next != null) { + next.shutdown(); + } + if (dispatcher != null) { + dispatcher.shutdown(); + } + if (indexService != null) { + indexService.shutdown(); + } + if (flatFileStore != null) { + flatFileStore.shutdown(); + } + if (storeExecutor != null) { + storeExecutor.shutdown(); + } + } + + @Override + public void destroy() { + if (next != null) { + next.destroy(); + } + if (indexService != null) { + indexService.destroy(); + } + if (flatFileStore != null) { + flatFileStore.destroy(); + } + if (metadataStore != null) { + metadataStore.destroy(); + } + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/common/AppendResult.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/common/AppendResult.java new file mode 100644 index 0000000..97cfe4d --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/common/AppendResult.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.tieredstore.common; + +public enum AppendResult { + + /** + * The append operation was successful. + */ + SUCCESS, + + /** + * The buffer used for the append operation is full. + */ + BUFFER_FULL, + + /** + * The file is full and cannot accept more data. + */ + FILE_FULL, + + /** + * The file is closed and cannot accept more data. + */ + FILE_CLOSED, + + /** + * An unknown error occurred during the append operation. + */ + UNKNOWN_ERROR +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/common/FileSegmentType.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/common/FileSegmentType.java new file mode 100644 index 0000000..d7b3c9a --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/common/FileSegmentType.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.tieredstore.common; + +import java.util.Arrays; + +public enum FileSegmentType { + + COMMIT_LOG(0), + + CONSUME_QUEUE(1), + + INDEX(2); + + private final int code; + + FileSegmentType(int code) { + this.code = code; + } + + public int getCode() { + return code; + } + + public static FileSegmentType valueOf(int fileType) { + return Arrays.stream(FileSegmentType.values()) + .filter(segmentType -> segmentType.getCode() == fileType) + .findFirst() + .orElse(COMMIT_LOG); + } +} \ No newline at end of file diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/common/GetMessageResultExt.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/common/GetMessageResultExt.java new file mode 100644 index 0000000..b6016f2 --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/common/GetMessageResultExt.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.tieredstore.common; + +import java.util.ArrayList; +import java.util.List; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.GetMessageStatus; +import org.apache.rocketmq.store.MessageFilter; +import org.apache.rocketmq.store.SelectMappedBufferResult; + +public class GetMessageResultExt extends GetMessageResult { + + private final List tagCodeList; + + public GetMessageResultExt() { + this.tagCodeList = new ArrayList<>(); + } + + public void addMessageExt(SelectMappedBufferResult bufferResult, long queueOffset, long tagCode) { + super.addMessage(bufferResult, queueOffset); + this.tagCodeList.add(tagCode); + } + + public List getTagCodeList() { + return tagCodeList; + } + + /** + * Due to the message fetched from the object storage is sequential, + * do message filtering occurs after the data retrieval. + */ + public GetMessageResult doFilterMessage(MessageFilter messageFilter) { + if (GetMessageStatus.FOUND != super.getStatus() || messageFilter == null) { + return this; + } + + GetMessageResult result = new GetMessageResult(); + result.setStatus(GetMessageStatus.FOUND); + result.setMinOffset(this.getMinOffset()); + result.setMaxOffset(this.getMaxOffset()); + result.setNextBeginOffset(this.getNextBeginOffset()); + + for (int i = 0; i < this.getMessageMapedList().size(); i++) { + if (!messageFilter.isMatchedByConsumeQueue(this.tagCodeList.get(i), null)) { + continue; + } + + SelectMappedBufferResult bufferResult = this.getMessageMapedList().get(i); + if (!messageFilter.isMatchedByCommitLog(bufferResult.getByteBuffer().slice(), null)) { + continue; + } + + long offset = this.getMessageQueueOffset().get(i); + result.addMessage(new SelectMappedBufferResult(bufferResult.getStartOffset(), + bufferResult.getByteBuffer().asReadOnlyBuffer(), bufferResult.getSize(), null), offset); + } + + if (result.getBufferTotalSize() == 0) { + result.setStatus(GetMessageStatus.NO_MATCHED_MESSAGE); + } + return result; + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/common/GroupCommitContext.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/common/GroupCommitContext.java new file mode 100644 index 0000000..f677e7c --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/common/GroupCommitContext.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.tieredstore.common; + +import java.util.List; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.SelectMappedBufferResult; + +public class GroupCommitContext { + + private long endOffset; + + private List bufferList; + + private List dispatchRequests; + + public long getEndOffset() { + return endOffset; + } + + public void setEndOffset(long endOffset) { + this.endOffset = endOffset; + } + + public List getBufferList() { + return bufferList; + } + + public void setBufferList(List bufferList) { + this.bufferList = bufferList; + } + + public List getDispatchRequests() { + return dispatchRequests; + } + + public void setDispatchRequests(List dispatchRequests) { + this.dispatchRequests = dispatchRequests; + } + + public void release() { + if (bufferList != null) { + for (SelectMappedBufferResult bufferResult : bufferList) { + bufferResult.release(); + } + bufferList.clear(); + bufferList = null; + } + if (dispatchRequests != null) { + dispatchRequests.clear(); + dispatchRequests = null; + } + + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/common/SelectBufferResult.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/common/SelectBufferResult.java new file mode 100644 index 0000000..cad37c7 --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/common/SelectBufferResult.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.tieredstore.common; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +public class SelectBufferResult { + + private final ByteBuffer byteBuffer; + private final long startOffset; + private final int size; + private final long tagCode; + private final AtomicLong accessCount; + + public SelectBufferResult(ByteBuffer byteBuffer, long startOffset, int size, long tagCode) { + this.startOffset = startOffset; + this.byteBuffer = byteBuffer; + this.size = size; + this.tagCode = tagCode; + this.accessCount = new AtomicLong(); + } + + public ByteBuffer getByteBuffer() { + return byteBuffer; + } + + public long getStartOffset() { + return startOffset; + } + + public int getSize() { + return size; + } + + public long getTagCode() { + return tagCode; + } + + public AtomicLong getAccessCount() { + return accessCount; + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/core/MessageStoreDispatcher.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/core/MessageStoreDispatcher.java new file mode 100644 index 0000000..e1e142a --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/core/MessageStoreDispatcher.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.tieredstore.core; + +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.store.CommitLogDispatcher; +import org.apache.rocketmq.tieredstore.file.FlatFileInterface; + +public interface MessageStoreDispatcher extends CommitLogDispatcher { + + void start(); + + void shutdown(); + + CompletableFuture doScheduleDispatch(FlatFileInterface flatFile, boolean force); +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/core/MessageStoreDispatcherImpl.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/core/MessageStoreDispatcherImpl.java new file mode 100644 index 0000000..bcc4e22 --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/core/MessageStoreDispatcherImpl.java @@ -0,0 +1,392 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tieredstore.core; + +import com.google.common.annotations.VisibleForTesting; +import io.opentelemetry.api.common.Attributes; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +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.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.exception.ConsumeQueueException; +import org.apache.rocketmq.store.queue.ConsumeQueueInterface; +import org.apache.rocketmq.store.queue.CqUnit; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.MessageStoreExecutor; +import org.apache.rocketmq.tieredstore.TieredMessageStore; +import org.apache.rocketmq.tieredstore.common.AppendResult; +import org.apache.rocketmq.tieredstore.common.FileSegmentType; +import org.apache.rocketmq.tieredstore.common.GroupCommitContext; +import org.apache.rocketmq.tieredstore.file.FlatFileInterface; +import org.apache.rocketmq.tieredstore.file.FlatFileStore; +import org.apache.rocketmq.tieredstore.index.IndexService; +import org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsConstant; +import org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsManager; +import org.apache.rocketmq.tieredstore.util.MessageFormatUtil; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MessageStoreDispatcherImpl extends ServiceThread implements MessageStoreDispatcher { + + protected static final Logger log = LoggerFactory.getLogger(MessageStoreUtil.TIERED_STORE_LOGGER_NAME); + + protected final String brokerName; + protected final MessageStore defaultStore; + protected final MessageStoreConfig storeConfig; + protected final TieredMessageStore messageStore; + protected final FlatFileStore flatFileStore; + protected final MessageStoreExecutor storeExecutor; + protected final MessageStoreFilter topicFilter; + protected final Semaphore semaphore; + protected final IndexService indexService; + protected final Map failedGroupCommitMap; + + public MessageStoreDispatcherImpl(TieredMessageStore messageStore) { + this.messageStore = messageStore; + this.storeConfig = messageStore.getStoreConfig(); + this.defaultStore = messageStore.getDefaultStore(); + this.brokerName = storeConfig.getBrokerName(); + this.semaphore = new Semaphore( + this.storeConfig.getTieredStoreMaxPendingLimit() / 4); + this.topicFilter = messageStore.getTopicFilter(); + this.flatFileStore = messageStore.getFlatFileStore(); + this.storeExecutor = messageStore.getStoreExecutor(); + this.indexService = messageStore.getIndexService(); + this.failedGroupCommitMap = new ConcurrentHashMap<>(); + } + + @Override + public String getServiceName() { + return MessageStoreDispatcher.class.getSimpleName(); + } + + @VisibleForTesting + public Map getFailedGroupCommitMap() { + return failedGroupCommitMap; + } + + public void dispatchWithSemaphore(FlatFileInterface flatFile) { + try { + if (stopped) { + return; + } + semaphore.acquire(); + this.doScheduleDispatch(flatFile, false) + .whenComplete((future, throwable) -> semaphore.release()); + } catch (Throwable t) { + semaphore.release(); + log.error("MessageStore dispatch error, topic={}, queueId={}", + flatFile.getMessageQueue().getTopic(), flatFile.getMessageQueue().getQueueId(), t); + } + } + + @Override + public void dispatch(DispatchRequest request) { + if (stopped || topicFilter != null && topicFilter.filterTopic(request.getTopic())) { + return; + } + flatFileStore.computeIfAbsent( + new MessageQueue(request.getTopic(), brokerName, request.getQueueId())); + } + + @Override + public CompletableFuture doScheduleDispatch(FlatFileInterface flatFile, boolean force) { + if (stopped) { + return CompletableFuture.completedFuture(true); + } + + String topic = flatFile.getMessageQueue().getTopic(); + int queueId = flatFile.getMessageQueue().getQueueId(); + + // For test scenarios, we set the 'force' variable to true to + // ensure that the data in the cache is directly committed successfully. + force = !storeConfig.isTieredStoreGroupCommit() || force; + if (force) { + flatFile.getFileLock().lock(); + } else { + if (!flatFile.getFileLock().tryLock()) { + return CompletableFuture.completedFuture(false); + } + } + + try { + if (topicFilter != null && topicFilter.filterTopic(flatFile.getMessageQueue().getTopic())) { + flatFileStore.destroyFile(flatFile.getMessageQueue()); + return CompletableFuture.completedFuture(false); + } + + long currentOffset = flatFile.getConsumeQueueMaxOffset(); + long commitOffset = flatFile.getConsumeQueueCommitOffset(); + long minOffsetInQueue = defaultStore.getMinOffsetInQueue(topic, queueId); + long maxOffsetInQueue = defaultStore.getMaxOffsetInQueue(topic, queueId); + + // If set to max offset here, some written messages may be lost + if (!flatFile.isFlatFileInit()) { + currentOffset = defaultStore.getOffsetInQueueByTime( + topic, queueId, System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(2)); + currentOffset = Math.max(currentOffset, minOffsetInQueue); + currentOffset = Math.min(currentOffset, maxOffsetInQueue); + flatFile.initOffset(currentOffset); + log.warn("MessageDispatcher#dispatch init, topic={}, queueId={}, offset={}-{}, current={}", + topic, queueId, minOffsetInQueue, maxOffsetInQueue, currentOffset); + return CompletableFuture.completedFuture(true); + } + + // If the previous commit fails, attempt to trigger a commit directly. + if (commitOffset < currentOffset) { + this.commitAsync(flatFile).whenComplete((result, throwable) -> { + if (throwable != null) { + log.error("MessageDispatcher#flatFile commitOffset less than currentOffset, commitAsync again failed. topic: {}, queueId: {} ", topic, queueId, throwable); + } + }); + return CompletableFuture.completedFuture(false); + } + + if (failedGroupCommitMap.containsKey(flatFile)) { + GroupCommitContext failedCommit = failedGroupCommitMap.get(flatFile); + if (failedCommit.getEndOffset() <= commitOffset) { + failedGroupCommitMap.remove(flatFile); + constructIndexFile(flatFile.getTopicId(), failedCommit); + } + } + + if (currentOffset < minOffsetInQueue) { + log.warn("MessageDispatcher#dispatch, current offset is too small, topic={}, queueId={}, offset={}-{}, current={}", + topic, queueId, minOffsetInQueue, maxOffsetInQueue, currentOffset); + flatFileStore.destroyFile(flatFile.getMessageQueue()); + flatFileStore.computeIfAbsent(new MessageQueue(topic, brokerName, queueId)); + return CompletableFuture.completedFuture(true); + } + + if (currentOffset > maxOffsetInQueue) { + log.warn("MessageDispatcher#dispatch, current offset is too large, topic={}, queueId={}, offset={}-{}, current={}", + topic, queueId, minOffsetInQueue, maxOffsetInQueue, currentOffset); + return CompletableFuture.completedFuture(false); + } + + long interval = TimeUnit.HOURS.toMillis(storeConfig.getCommitLogRollingInterval()); + if (flatFile.rollingFile(interval)) { + log.info("MessageDispatcher#dispatch, rolling file, topic={}, queueId={}, offset={}-{}, current={}", + topic, queueId, minOffsetInQueue, maxOffsetInQueue, currentOffset); + } + + if (currentOffset == maxOffsetInQueue) { + return CompletableFuture.completedFuture(false); + } + + long bufferSize = 0L; + long groupCommitSize = storeConfig.getTieredStoreGroupCommitSize(); + long groupCommitCount = storeConfig.getTieredStoreGroupCommitCount(); + long targetOffset = Math.min(currentOffset + groupCommitCount, maxOffsetInQueue); + + ConsumeQueueInterface consumeQueue = defaultStore.getConsumeQueue(topic, queueId); + CqUnit cqUnit = consumeQueue.get(currentOffset); + if (cqUnit == null) { + log.warn("MessageDispatcher#dispatch cq not found, topic={}, queueId={}, offset={}-{}, current={}, remain={}", + topic, queueId, minOffsetInQueue, maxOffsetInQueue, currentOffset, maxOffsetInQueue - currentOffset); + return CompletableFuture.completedFuture(false); + } + + SelectMappedBufferResult message = + defaultStore.selectOneMessageByOffset(cqUnit.getPos(), cqUnit.getSize()); + if (message == null) { + log.warn("MessageDispatcher#dispatch message not found, topic={}, queueId={}, offset={}-{}, current={}, remain={}", + topic, queueId, minOffsetInQueue, maxOffsetInQueue, currentOffset, maxOffsetInQueue - currentOffset); + return CompletableFuture.completedFuture(false); + } + + boolean timeout = MessageFormatUtil.getStoreTimeStamp(message.getByteBuffer()) + + storeConfig.getTieredStoreGroupCommitTimeout() < System.currentTimeMillis(); + boolean bufferFull = maxOffsetInQueue - currentOffset > storeConfig.getTieredStoreGroupCommitCount(); + + if (!timeout && !bufferFull && !force) { + log.debug("MessageDispatcher#dispatch hold, topic={}, queueId={}, offset={}-{}, current={}, remain={}", + topic, queueId, minOffsetInQueue, maxOffsetInQueue, currentOffset, maxOffsetInQueue - currentOffset); + message.release(); + return CompletableFuture.completedFuture(false); + } else { + if (MessageFormatUtil.getStoreTimeStamp(message.getByteBuffer()) + + TimeUnit.MINUTES.toMillis(5) < System.currentTimeMillis()) { + log.warn("MessageDispatcher#dispatch behind too much, topic={}, queueId={}, offset={}-{}, current={}, remain={}", + topic, queueId, minOffsetInQueue, maxOffsetInQueue, currentOffset, maxOffsetInQueue - currentOffset); + } else { + log.info("MessageDispatcher#dispatch success, topic={}, queueId={}, offset={}-{}, current={}, remain={}", + topic, queueId, minOffsetInQueue, maxOffsetInQueue, currentOffset, maxOffsetInQueue - currentOffset); + } + message.release(); + } + + long offset = currentOffset; + List appendingBufferList = new ArrayList<>(); + List dispatchRequestList = new ArrayList<>(); + for (; offset < targetOffset; offset++) { + cqUnit = consumeQueue.get(offset); + bufferSize += cqUnit.getSize(); + if (bufferSize >= groupCommitSize) { + break; + } + message = defaultStore.selectOneMessageByOffset(cqUnit.getPos(), cqUnit.getSize()); + appendingBufferList.add(message); + + ByteBuffer byteBuffer = message.getByteBuffer(); + AppendResult result = flatFile.appendCommitLog(message); + if (!AppendResult.SUCCESS.equals(result)) { + break; + } + + long mappedCommitLogOffset = flatFile.getCommitLogMaxOffset() - byteBuffer.remaining(); + Map properties = MessageFormatUtil.getProperties(byteBuffer); + + DispatchRequest dispatchRequest = new DispatchRequest(topic, queueId, mappedCommitLogOffset, + cqUnit.getSize(), cqUnit.getTagsCode(), MessageFormatUtil.getStoreTimeStamp(byteBuffer), + cqUnit.getQueueOffset(), properties.getOrDefault(MessageConst.PROPERTY_KEYS, ""), + properties.getOrDefault(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, ""), + 0, 0, new HashMap<>()); + dispatchRequest.setOffsetId(MessageFormatUtil.getOffsetId(byteBuffer)); + + result = flatFile.appendConsumeQueue(dispatchRequest); + if (!AppendResult.SUCCESS.equals(result)) { + break; + } else { + dispatchRequestList.add(dispatchRequest); + } + } + + GroupCommitContext groupCommitContext = new GroupCommitContext(); + groupCommitContext.setEndOffset(offset); + groupCommitContext.setBufferList(appendingBufferList); + groupCommitContext.setDispatchRequests(dispatchRequestList); + + // If there are many messages waiting to be uploaded, call the upload logic immediately. + boolean repeat = timeout || maxOffsetInQueue - offset > storeConfig.getTieredStoreGroupCommitCount(); + + if (!dispatchRequestList.isEmpty()) { + Attributes attributes = TieredStoreMetricsManager.newAttributesBuilder() + .put(TieredStoreMetricsConstant.LABEL_TOPIC, topic) + .put(TieredStoreMetricsConstant.LABEL_QUEUE_ID, queueId) + .put(TieredStoreMetricsConstant.LABEL_FILE_TYPE, FileSegmentType.COMMIT_LOG.name().toLowerCase()) + .build(); + TieredStoreMetricsManager.messagesDispatchTotal.add(offset - currentOffset, attributes); + + this.commitAsync(flatFile).whenComplete((success, throwable) -> { + if (success) { + constructIndexFile(flatFile.getTopicId(), groupCommitContext); + } + else { + //next commit async,execute constructIndexFile. + GroupCommitContext oldCommit = failedGroupCommitMap.put(flatFile, groupCommitContext); + if (oldCommit != null) { + log.warn("MessageDispatcher#commitAsync failed,flatFile old failed commit context not release, topic={}, queueId={} ", topic, queueId); + oldCommit.release(); + } + } + if (success && repeat) { + storeExecutor.commonExecutor.submit(() -> dispatchWithSemaphore(flatFile)); + } + } + ); + } + } catch (ConsumeQueueException e) { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(e); + return future; + } finally { + flatFile.getFileLock().unlock(); + } + return CompletableFuture.completedFuture(false); + } + + public CompletableFuture commitAsync(FlatFileInterface flatFile) { + return flatFile.commitAsync(); + } + + public void constructIndexFile(long topicId, GroupCommitContext groupCommitContext) { + MessageStoreExecutor.getInstance().bufferCommitExecutor.submit(() -> { + if (storeConfig.isMessageIndexEnable()) { + try { + groupCommitContext.getDispatchRequests().forEach(request -> constructIndexFile0(topicId, request)); + } + catch (Throwable e) { + log.error("constructIndexFile error {}", topicId, e); + } + } + groupCommitContext.release(); + }); + } + + /** + * Building indexes with offsetId is no longer supported because offsetId has changed in tiered storage + */ + public void constructIndexFile0(long topicId, DispatchRequest request) { + Set keySet = new HashSet<>(); + if (StringUtils.isNotBlank(request.getUniqKey())) { + keySet.add(request.getUniqKey()); + } + if (StringUtils.isNotBlank(request.getKeys())) { + keySet.addAll(Arrays.asList(request.getKeys().split(MessageConst.KEY_SEPARATOR))); + } + indexService.putKey(request.getTopic(), (int) topicId, request.getQueueId(), keySet, + request.getCommitLogOffset(), request.getMsgSize(), request.getStoreTimestamp()); + } + + public void releaseClosedPendingGroupCommit() { + Iterator> iterator = failedGroupCommitMap.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (entry.getKey().isClosed()) { + entry.getValue().release(); + iterator.remove(); + } + } + } + + + @Override + public void run() { + log.info("{} service started", this.getServiceName()); + while (!this.isStopped()) { + try { + flatFileStore.deepCopyFlatFileToList().forEach(this::dispatchWithSemaphore); + + releaseClosedPendingGroupCommit(); + + this.waitForRunning(Duration.ofSeconds(20).toMillis()); + } catch (Throwable t) { + log.error("MessageStore dispatch error", t); + } + } + log.info("{} service shutdown", this.getServiceName()); + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/core/MessageStoreFetcher.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/core/MessageStoreFetcher.java new file mode 100644 index 0000000..8e2e8bd --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/core/MessageStoreFetcher.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.tieredstore.core; + +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.MessageFilter; +import org.apache.rocketmq.store.QueryMessageResult; + +public interface MessageStoreFetcher { + + /** + * Asynchronous get the store time of the earliest message in this store. + * + * @return timestamp of the earliest message in this store. + */ + CompletableFuture getEarliestMessageTimeAsync(String topic, int queueId); + + /** + * Asynchronous get the store time of the message specified. + * + * @param topic Message topic. + * @param queueId Queue ID. + * @param consumeQueueOffset Consume queue offset. + * @return store timestamp of the message. + */ + CompletableFuture getMessageStoreTimeStampAsync(String topic, int queueId, long consumeQueueOffset); + + /** + * 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 timestamp Timestamp to look up. + * @return physical offset which matches. + */ + long getOffsetInQueueByTime(String topic, int queueId, long timestamp, BoundaryType type); + + /** + * Asynchronous get message + * + * @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 maxCount Maximum count of messages to query. + * @param messageFilter Message filter used to screen desired messages. + * @return Matched messages. + */ + CompletableFuture getMessageAsync( + String group, String topic, int queueId, long offset, int maxCount, MessageFilter messageFilter); + + /** + * Asynchronous query messages by given key. + * + * @param topic Topic of the message. + * @param key Message key. + * @param maxCount Maximum count of the messages possible. + * @param begin Begin timestamp. + * @param end End timestamp. + */ + CompletableFuture queryMessageAsync( + String topic, String key, int maxCount, long begin, long end); +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/core/MessageStoreFetcherImpl.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/core/MessageStoreFetcherImpl.java new file mode 100644 index 0000000..9e5ab01 --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/core/MessageStoreFetcherImpl.java @@ -0,0 +1,463 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tieredstore.core; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.Scheduler; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.GetMessageStatus; +import org.apache.rocketmq.store.MessageFilter; +import org.apache.rocketmq.store.QueryMessageResult; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.TieredMessageStore; +import org.apache.rocketmq.tieredstore.common.GetMessageResultExt; +import org.apache.rocketmq.tieredstore.common.SelectBufferResult; +import org.apache.rocketmq.tieredstore.exception.TieredStoreException; +import org.apache.rocketmq.tieredstore.file.FlatFileStore; +import org.apache.rocketmq.tieredstore.file.FlatMessageFile; +import org.apache.rocketmq.tieredstore.index.IndexItem; +import org.apache.rocketmq.tieredstore.index.IndexService; +import org.apache.rocketmq.tieredstore.metadata.MetadataStore; +import org.apache.rocketmq.tieredstore.metadata.entity.TopicMetadata; +import org.apache.rocketmq.tieredstore.util.MessageFormatUtil; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MessageStoreFetcherImpl implements MessageStoreFetcher { + + private static final Logger log = LoggerFactory.getLogger(MessageStoreUtil.TIERED_STORE_LOGGER_NAME); + + protected static final String CACHE_KEY_FORMAT = "%s@%d@%d"; + + private final String brokerName; + private final MetadataStore metadataStore; + private final MessageStoreConfig storeConfig; + private final TieredMessageStore messageStore; + private final IndexService indexService; + private final FlatFileStore flatFileStore; + private final long memoryMaxSize; + private final Cache fetcherCache; + + public MessageStoreFetcherImpl(TieredMessageStore messageStore) { + this(messageStore, messageStore.getStoreConfig(), + messageStore.getFlatFileStore(), messageStore.getIndexService()); + } + + public MessageStoreFetcherImpl(TieredMessageStore messageStore, MessageStoreConfig storeConfig, + FlatFileStore flatFileStore, IndexService indexService) { + + this.storeConfig = storeConfig; + this.brokerName = storeConfig.getBrokerName(); + this.flatFileStore = flatFileStore; + this.messageStore = messageStore; + this.indexService = indexService; + this.metadataStore = flatFileStore.getMetadataStore(); + this.memoryMaxSize = + (long) (Runtime.getRuntime().maxMemory() * storeConfig.getReadAheadCacheSizeThresholdRate()); + this.fetcherCache = this.initCache(storeConfig); + log.info("MessageStoreFetcher init success, brokerName={}", storeConfig.getBrokerName()); + } + + private Cache initCache(MessageStoreConfig storeConfig) { + + return Caffeine.newBuilder() + .scheduler(Scheduler.systemScheduler()) + // Clients may repeatedly request messages at the same offset in tiered storage, + // causing the request queue to become full. Using expire after read or write policy + // to refresh the cache expiration time. + .expireAfterAccess(storeConfig.getReadAheadCacheExpireDuration(), TimeUnit.MILLISECONDS) + .maximumWeight(memoryMaxSize) + // Using the buffer size of messages to calculate memory usage + .weigher((String key, SelectBufferResult buffer) -> buffer.getSize()) + .recordStats() + .build(); + } + + public Cache getFetcherCache() { + return fetcherCache; + } + + protected void putMessageToCache(FlatMessageFile flatFile, long offset, SelectBufferResult result) { + MessageQueue mq = flatFile.getMessageQueue(); + this.fetcherCache.put(String.format(CACHE_KEY_FORMAT, mq.getTopic(), mq.getQueueId(), offset), result); + } + + protected SelectBufferResult getMessageFromCache(FlatMessageFile flatFile, long offset) { + MessageQueue mq = flatFile.getMessageQueue(); + SelectBufferResult buffer = this.fetcherCache.getIfPresent( + String.format(CACHE_KEY_FORMAT, mq.getTopic(), mq.getQueueId(), offset)); + // return duplicate buffer here + if (buffer == null) { + return null; + } + long count = buffer.getAccessCount().incrementAndGet(); + if (count % 1000L == 0L) { + log.warn("MessageFetcher fetch same offset message too many times, " + + "topic={}, queueId={}, offset={}, count={}", mq.getTopic(), mq.getQueueId(), offset, count); + } + return new SelectBufferResult( + buffer.getByteBuffer().asReadOnlyBuffer(), buffer.getStartOffset(), buffer.getSize(), buffer.getTagCode()); + } + + protected GetMessageResultExt getMessageFromCache( + FlatMessageFile flatFile, long offset, int maxCount, MessageFilter messageFilter) { + GetMessageResultExt result = new GetMessageResultExt(); + int interval = storeConfig.getReadAheadMessageCountThreshold(); + for (long current = offset, end = offset + interval; current < end; current++) { + SelectBufferResult buffer = getMessageFromCache(flatFile, current); + if (buffer == null) { + result.setNextBeginOffset(current); + break; + } + result.setNextBeginOffset(current + 1); + if (messageFilter != null) { + if (!messageFilter.isMatchedByConsumeQueue(buffer.getTagCode(), null)) { + continue; + } + if (!messageFilter.isMatchedByCommitLog(buffer.getByteBuffer().slice(), null)) { + continue; + } + } + SelectMappedBufferResult bufferResult = new SelectMappedBufferResult( + buffer.getStartOffset(), buffer.getByteBuffer(), buffer.getSize(), null); + result.addMessageExt(bufferResult, current, buffer.getTagCode()); + if (result.getMessageCount() == maxCount) { + break; + } + long maxTransferBytes = messageStore.getMessageStoreConfig().getMaxTransferBytesOnMessageInMemory(); + if (result.getBufferTotalSize() >= maxTransferBytes) { + break; + } + } + result.setStatus(result.getMessageCount() > 0 ? + GetMessageStatus.FOUND : GetMessageStatus.NO_MATCHED_MESSAGE); + result.setMinOffset(flatFile.getConsumeQueueMinOffset()); + result.setMaxOffset(flatFile.getConsumeQueueCommitOffset()); + return result; + } + + protected CompletableFuture fetchMessageThenPutToCache( + FlatMessageFile flatFile, long queueOffset, int batchSize) { + + MessageQueue mq = flatFile.getMessageQueue(); + return this.getMessageFromTieredStoreAsync(flatFile, queueOffset, batchSize) + .thenApply(result -> { + if (result.getStatus() == GetMessageStatus.OFFSET_OVERFLOW_ONE || + result.getStatus() == GetMessageStatus.OFFSET_OVERFLOW_BADLY) { + return -1L; + } + if (result.getStatus() != GetMessageStatus.FOUND) { + log.warn("MessageFetcher prefetch message then put to cache failed, result={}, " + + "topic={}, queue={}, queue offset={}, batch size={}", + result.getStatus(), mq.getTopic(), mq.getQueueId(), queueOffset, batchSize); + return -1L; + } + List offsetList = result.getMessageQueueOffset(); + List tagCodeList = result.getTagCodeList(); + List msgList = result.getMessageMapedList(); + for (int i = 0; i < offsetList.size(); i++) { + SelectMappedBufferResult msg = msgList.get(i); + SelectBufferResult bufferResult = new SelectBufferResult( + msg.getByteBuffer(), msg.getStartOffset(), msg.getSize(), tagCodeList.get(i)); + this.putMessageToCache(flatFile, queueOffset + i, bufferResult); + } + return offsetList.get(offsetList.size() - 1); + }); + } + + public CompletableFuture getMessageFromCacheAsync( + FlatMessageFile flatFile, String group, long queueOffset, int maxCount, MessageFilter messageFilter) { + + MessageQueue mq = flatFile.getMessageQueue(); + GetMessageResultExt result = getMessageFromCache(flatFile, queueOffset, maxCount, messageFilter); + + if (GetMessageStatus.FOUND.equals(result.getStatus())) { + log.debug("MessageFetcher cache hit, group={}, topic={}, queueId={}, offset={}, maxCount={}, resultSize={}, lag={}", + group, mq.getTopic(), mq.getQueueId(), queueOffset, maxCount, + result.getMessageCount(), result.getMaxOffset() - result.getNextBeginOffset()); + return CompletableFuture.completedFuture(result); + } + + // If cache miss, pull messages immediately + log.debug("MessageFetcher cache miss, group={}, topic={}, queueId={}, offset={}, maxCount={}, lag={}", + group, mq.getTopic(), mq.getQueueId(), queueOffset, maxCount, result.getMaxOffset() - result.getNextBeginOffset()); + + // To optimize the performance of pop consumption + // Pop revive will cause a large number of random reads, + // so the amount of pre-fetch message num needs to be reduced. + int fetchSize = maxCount == 1 ? 32 : storeConfig.getReadAheadMessageCountThreshold(); + return fetchMessageThenPutToCache(flatFile, queueOffset, fetchSize) + .thenApply(maxOffset -> getMessageFromCache(flatFile, queueOffset, maxCount, messageFilter)); + } + + public CompletableFuture getMessageFromTieredStoreAsync( + FlatMessageFile flatFile, long queueOffset, int batchSize) { + + GetMessageResultExt result = new GetMessageResultExt(); + result.setMinOffset(flatFile.getConsumeQueueMinOffset()); + result.setMaxOffset(flatFile.getConsumeQueueCommitOffset()); + + if (queueOffset < result.getMinOffset()) { + result.setStatus(GetMessageStatus.OFFSET_TOO_SMALL); + result.setNextBeginOffset(result.getMinOffset()); + return CompletableFuture.completedFuture(result); + } else if (queueOffset == result.getMaxOffset()) { + result.setStatus(GetMessageStatus.OFFSET_OVERFLOW_ONE); + result.setNextBeginOffset(queueOffset); + return CompletableFuture.completedFuture(result); + } else if (queueOffset > result.getMaxOffset()) { + result.setStatus(GetMessageStatus.OFFSET_OVERFLOW_BADLY); + result.setNextBeginOffset(result.getMaxOffset()); + return CompletableFuture.completedFuture(result); + } + + if (queueOffset < result.getMaxOffset()) { + batchSize = Math.min(batchSize, (int) Math.min( + result.getMaxOffset() - queueOffset, storeConfig.getReadAheadMessageCountThreshold())); + } + + CompletableFuture readConsumeQueueFuture; + try { + readConsumeQueueFuture = flatFile.getConsumeQueueAsync(queueOffset, batchSize); + } catch (TieredStoreException e) { + switch (e.getErrorCode()) { + case ILLEGAL_PARAM: + case ILLEGAL_OFFSET: + default: + result.setStatus(GetMessageStatus.OFFSET_FOUND_NULL); + result.setNextBeginOffset(queueOffset); + return CompletableFuture.completedFuture(result); + } + } + + int finalBatchSize = batchSize; + CompletableFuture readCommitLogFuture = readConsumeQueueFuture.thenCompose(cqBuffer -> { + + long firstCommitLogOffset = MessageFormatUtil.getCommitLogOffsetFromItem(cqBuffer); + cqBuffer.position(cqBuffer.remaining() - MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE); + long lastCommitLogOffset = MessageFormatUtil.getCommitLogOffsetFromItem(cqBuffer); + if (lastCommitLogOffset < firstCommitLogOffset) { + log.error("MessageFetcher#getMessageFromTieredStoreAsync, last offset is smaller than first offset, " + + "topic={} queueId={}, offset={}, firstOffset={}, lastOffset={}", + flatFile.getMessageQueue().getTopic(), flatFile.getMessageQueue().getQueueId(), queueOffset, + firstCommitLogOffset, lastCommitLogOffset); + return CompletableFuture.completedFuture(ByteBuffer.allocate(0)); + } + + // Get at least one message + // Reducing the length limit of cq to prevent OOM + long length = lastCommitLogOffset - firstCommitLogOffset + MessageFormatUtil.getSizeFromItem(cqBuffer); + while (cqBuffer.limit() > MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE && + length > storeConfig.getReadAheadMessageSizeThreshold()) { + cqBuffer.limit(cqBuffer.position()); + cqBuffer.position(cqBuffer.limit() - MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE); + length = MessageFormatUtil.getCommitLogOffsetFromItem(cqBuffer) + - firstCommitLogOffset + MessageFormatUtil.getSizeFromItem(cqBuffer); + } + int messageCount = cqBuffer.position() / MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE + 1; + + log.info("MessageFetcher#getMessageFromTieredStoreAsync, " + + "topic={}, queueId={}, broker offset={}-{}, offset={}, expect={}, actually={}, lag={}", + flatFile.getMessageQueue().getTopic(), flatFile.getMessageQueue().getQueueId(), + result.getMinOffset(), result.getMaxOffset(), queueOffset, finalBatchSize, + messageCount, result.getMaxOffset() - queueOffset); + + return flatFile.getCommitLogAsync(firstCommitLogOffset, (int) length); + }); + + return readConsumeQueueFuture.thenCombine(readCommitLogFuture, (cqBuffer, msgBuffer) -> { + List bufferList = MessageFormatUtil.splitMessageBuffer(cqBuffer, msgBuffer); + int requestSize = cqBuffer.remaining() / MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE; + + // not use buffer list size to calculate next offset to prevent split error + if (bufferList.isEmpty()) { + result.setStatus(GetMessageStatus.NO_MATCHED_MESSAGE); + result.setNextBeginOffset(queueOffset + requestSize); + } else { + result.setStatus(GetMessageStatus.FOUND); + result.setNextBeginOffset(queueOffset + requestSize); + + for (SelectBufferResult bufferResult : bufferList) { + ByteBuffer slice = bufferResult.getByteBuffer().slice(); + slice.limit(bufferResult.getSize()); + SelectMappedBufferResult msg = new SelectMappedBufferResult(bufferResult.getStartOffset(), + bufferResult.getByteBuffer(), bufferResult.getSize(), null); + result.addMessageExt(msg, MessageFormatUtil.getQueueOffset(slice), bufferResult.getTagCode()); + } + } + return result; + }).exceptionally(e -> { + MessageQueue mq = flatFile.getMessageQueue(); + log.warn("MessageFetcher#getMessageFromTieredStoreAsync failed, " + + "topic={} queueId={}, offset={}, batchSize={}", mq.getTopic(), mq.getQueueId(), queueOffset, finalBatchSize, e); + result.setStatus(GetMessageStatus.OFFSET_FOUND_NULL); + result.setNextBeginOffset(queueOffset); + return result; + }); + } + + @Override + public CompletableFuture getMessageAsync( + String group, String topic, int queueId, long queueOffset, int maxCount, final MessageFilter messageFilter) { + + GetMessageResult result = new GetMessageResult(); + FlatMessageFile flatFile = flatFileStore.getFlatFile(new MessageQueue(topic, brokerName, queueId)); + + if (flatFile == null) { + result.setNextBeginOffset(queueOffset); + result.setStatus(GetMessageStatus.NO_MATCHED_LOGIC_QUEUE); + return CompletableFuture.completedFuture(result); + } + + // Max queue offset means next message put position + result.setMinOffset(flatFile.getConsumeQueueMinOffset()); + result.setMaxOffset(flatFile.getConsumeQueueCommitOffset()); + + // Fill result according file offset. + // Offset range | Result | Fix to + // (-oo, 0] | no message | current offset + // (0, min) | too small | min offset + // [min, max) | correct | + // [max, max] | overflow one | max offset + // (max, +oo) | overflow badly | max offset + + if (result.getMaxOffset() <= 0) { + result.setStatus(GetMessageStatus.NO_MESSAGE_IN_QUEUE); + result.setNextBeginOffset(queueOffset); + return CompletableFuture.completedFuture(result); + } else if (queueOffset < result.getMinOffset()) { + result.setStatus(GetMessageStatus.OFFSET_TOO_SMALL); + result.setNextBeginOffset(result.getMinOffset()); + return CompletableFuture.completedFuture(result); + } else if (queueOffset == result.getMaxOffset()) { + result.setStatus(GetMessageStatus.OFFSET_OVERFLOW_ONE); + result.setNextBeginOffset(result.getMaxOffset()); + return CompletableFuture.completedFuture(result); + } else if (queueOffset > result.getMaxOffset()) { + result.setStatus(GetMessageStatus.OFFSET_OVERFLOW_BADLY); + result.setNextBeginOffset(result.getMaxOffset()); + return CompletableFuture.completedFuture(result); + } + + boolean cacheBusy = fetcherCache.estimatedSize() > memoryMaxSize * 0.8; + if (storeConfig.isReadAheadCacheEnable() && !cacheBusy) { + return getMessageFromCacheAsync(flatFile, group, queueOffset, maxCount, messageFilter); + } else { + return getMessageFromTieredStoreAsync(flatFile, queueOffset, maxCount) + .thenApply(messageResultExt -> messageResultExt.doFilterMessage(messageFilter)); + } + } + + @Override + public CompletableFuture getEarliestMessageTimeAsync(String topic, int queueId) { + FlatMessageFile flatFile = flatFileStore.getFlatFile(new MessageQueue(topic, brokerName, queueId)); + return CompletableFuture.completedFuture(flatFile == null ? -1L : flatFile.getMinStoreTimestamp()); + } + + @Override + public CompletableFuture getMessageStoreTimeStampAsync(String topic, int queueId, long queueOffset) { + FlatMessageFile flatFile = flatFileStore.getFlatFile(new MessageQueue(topic, brokerName, queueId)); + if (flatFile == null) { + return CompletableFuture.completedFuture(-1L); + } + + return flatFile.getConsumeQueueAsync(queueOffset) + .thenComposeAsync(cqItem -> { + long commitLogOffset = MessageFormatUtil.getCommitLogOffsetFromItem(cqItem); + int size = MessageFormatUtil.getSizeFromItem(cqItem); + return flatFile.getCommitLogAsync(commitLogOffset, size); + }, messageStore.getStoreExecutor().bufferFetchExecutor) + .thenApply(MessageFormatUtil::getStoreTimeStamp) + .exceptionally(e -> { + log.error("MessageStoreFetcherImpl#getMessageStoreTimeStampAsync: " + + "get or decode message failed, topic={}, queue={}, offset={}", topic, queueId, queueOffset, e); + return -1L; + }); + } + + @Override + public long getOffsetInQueueByTime(String topic, int queueId, long timestamp, BoundaryType type) { + FlatMessageFile flatFile = flatFileStore.getFlatFile(new MessageQueue(topic, brokerName, queueId)); + if (flatFile == null) { + return -1L; + } + return flatFile.getQueueOffsetByTimeAsync(timestamp, type).join(); + } + + @Override + public CompletableFuture queryMessageAsync( + String topic, String key, int maxCount, long begin, long end) { + + long topicId; + try { + TopicMetadata topicMetadata = metadataStore.getTopic(topic); + if (topicMetadata == null) { + log.info("MessageFetcher#queryMessageAsync, topic metadata not found, topic={}", topic); + return CompletableFuture.completedFuture(new QueryMessageResult()); + } + topicId = topicMetadata.getTopicId(); + } catch (Exception e) { + log.error("MessageFetcher#queryMessageAsync, get topic id failed, topic={}", topic, e); + return CompletableFuture.completedFuture(new QueryMessageResult()); + } + + CompletableFuture> future = indexService.queryAsync(topic, key, maxCount, begin, end); + + return future.thenCompose(indexItemList -> { + List> futureList = new ArrayList<>(maxCount); + for (IndexItem indexItem : indexItemList) { + if (topicId != indexItem.getTopicId()) { + continue; + } + FlatMessageFile flatFile = + flatFileStore.getFlatFile(new MessageQueue(topic, brokerName, indexItem.getQueueId())); + if (flatFile == null) { + continue; + } + CompletableFuture getMessageFuture = flatFile + .getCommitLogAsync(indexItem.getOffset(), indexItem.getSize()) + .thenApply(messageBuffer -> new SelectMappedBufferResult( + indexItem.getOffset(), messageBuffer, indexItem.getSize(), null)); + futureList.add(getMessageFuture); + if (futureList.size() >= maxCount) { + break; + } + } + return CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).thenApply(v -> { + QueryMessageResult result = new QueryMessageResult(); + futureList.forEach(f -> f.thenAccept(result::addMessage)); + return result; + }); + }).whenComplete((result, throwable) -> { + if (result != null) { + log.info("MessageFetcher#queryMessageAsync, " + + "query result={}, topic={}, topicId={}, key={}, maxCount={}, timestamp={}-{}", + result.getMessageBufferList().size(), topic, topicId, key, maxCount, begin, end); + } + }); + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/core/MessageStoreFilter.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/core/MessageStoreFilter.java new file mode 100644 index 0000000..524761f --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/core/MessageStoreFilter.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.tieredstore.core; + +public interface MessageStoreFilter { + + boolean filterTopic(String topicName); + + void addTopicToBlackList(String topicName); +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/core/MessageStoreTopicFilter.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/core/MessageStoreTopicFilter.java new file mode 100644 index 0000000..b64f163 --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/core/MessageStoreTopicFilter.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.tieredstore.core; + +import java.util.HashSet; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.PopAckConstants; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; + +public class MessageStoreTopicFilter implements MessageStoreFilter { + + private final Set topicBlackSet; + + public MessageStoreTopicFilter(MessageStoreConfig storeConfig) { + this.topicBlackSet = new HashSet<>(); + this.topicBlackSet.add(storeConfig.getBrokerClusterName()); + this.topicBlackSet.add(storeConfig.getBrokerName()); + } + + @Override + public boolean filterTopic(String topicName) { + if (StringUtils.isBlank(topicName)) { + return true; + } + return TopicValidator.isSystemTopic(topicName) || + PopAckConstants.isStartWithRevivePrefix(topicName) || + this.topicBlackSet.contains(topicName) || + MixAll.isLmq(topicName); + } + + @Override + public void addTopicToBlackList(String topicName) { + this.topicBlackSet.add(topicName); + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/exception/TieredStoreErrorCode.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/exception/TieredStoreErrorCode.java new file mode 100644 index 0000000..d29025f --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/exception/TieredStoreErrorCode.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.tieredstore.exception; + +public enum TieredStoreErrorCode { + + /** + * Error code for an invalid offset. + */ + ILLEGAL_OFFSET, + + /** + * Error code for an invalid parameter. + */ + ILLEGAL_PARAM, + + /** + * Error code for an incorrect download length. + */ + DOWNLOAD_LENGTH_NOT_CORRECT, + + /** + * Error code for no new data found in the storage system. + */ + NO_NEW_DATA, + + /** + * Error code for a storage provider error. + */ + STORAGE_PROVIDER_ERROR, + + /** + * Error code for an input/output error. + */ + IO_ERROR, + + /** + * Segment has been sealed + */ + SEGMENT_SEALED, + + /** + * Error code for an unknown error. + */ + UNKNOWN +} \ No newline at end of file diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/exception/TieredStoreException.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/exception/TieredStoreException.java new file mode 100644 index 0000000..3841643 --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/exception/TieredStoreException.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.tieredstore.exception; + +public class TieredStoreException extends RuntimeException { + + private final TieredStoreErrorCode errorCode; + private String requestId; + private long position = -1L; + + public TieredStoreException(TieredStoreErrorCode errorCode, String errorMessage) { + super(errorMessage); + this.errorCode = errorCode; + } + + public TieredStoreErrorCode getErrorCode() { + return errorCode; + } + + public String getRequestId() { + return requestId; + } + + public void setRequestId(String requestId) { + this.requestId = requestId; + } + + public long getPosition() { + return position; + } + + public void setPosition(long position) { + this.position = position; + } + + @Override + public String toString() { + StringBuilder errorStringBuilder = new StringBuilder(super.toString()); + if (requestId != null) { + errorStringBuilder.append(" requestId: ").append(requestId); + } + if (position != -1L) { + errorStringBuilder.append(", position: ").append(position); + } + return errorStringBuilder.toString(); + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/file/FlatAppendFile.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/file/FlatAppendFile.java new file mode 100644 index 0000000..38e451d --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/file/FlatAppendFile.java @@ -0,0 +1,277 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tieredstore.file; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.Collectors; +import org.apache.rocketmq.tieredstore.common.AppendResult; +import org.apache.rocketmq.tieredstore.common.FileSegmentType; +import org.apache.rocketmq.tieredstore.metadata.MetadataStore; +import org.apache.rocketmq.tieredstore.metadata.entity.FileSegmentMetadata; +import org.apache.rocketmq.tieredstore.provider.FileSegment; +import org.apache.rocketmq.tieredstore.provider.FileSegmentFactory; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FlatAppendFile { + + protected static final Logger log = LoggerFactory.getLogger(MessageStoreUtil.TIERED_STORE_LOGGER_NAME); + public static final long GET_FILE_SIZE_ERROR = -1L; + + protected final String filePath; + protected final FileSegmentType fileType; + protected final MetadataStore metadataStore; + protected final FileSegmentFactory fileSegmentFactory; + protected final ReentrantReadWriteLock fileSegmentLock; + protected final CopyOnWriteArrayList fileSegmentTable; + + protected FlatAppendFile(FileSegmentFactory fileSegmentFactory, FileSegmentType fileType, String filePath) { + + this.fileType = fileType; + this.filePath = filePath; + this.metadataStore = fileSegmentFactory.getMetadataStore(); + this.fileSegmentFactory = fileSegmentFactory; + this.fileSegmentLock = new ReentrantReadWriteLock(); + this.fileSegmentTable = new CopyOnWriteArrayList<>(); + this.recover(); + this.recoverFileSize(); + } + + public void recover() { + List fileSegmentList = new ArrayList<>(); + this.metadataStore.iterateFileSegment(this.filePath, this.fileType, metadata -> { + FileSegment fileSegment = this.fileSegmentFactory.createSegment( + this.fileType, metadata.getPath(), metadata.getBaseOffset()); + fileSegment.initPosition(metadata.getSize()); + fileSegment.setMinTimestamp(metadata.getBeginTimestamp()); + fileSegment.setMaxTimestamp(metadata.getEndTimestamp()); + fileSegmentList.add(fileSegment); + }); + this.fileSegmentTable.addAll(fileSegmentList.stream().sorted().collect(Collectors.toList())); + } + + public void recoverFileSize() { + if (fileSegmentTable.isEmpty() || FileSegmentType.INDEX.equals(fileType)) { + return; + } + FileSegment fileSegment = fileSegmentTable.get(fileSegmentTable.size() - 1); + long fileSize = fileSegment.getSize(); + if (fileSize == GET_FILE_SIZE_ERROR) { + log.warn("FlatAppendFile get last file size error, filePath: {}", this.filePath); + return; + } + if (fileSegment.getCommitPosition() != fileSize) { + fileSegment.initPosition(fileSize); + flushFileSegmentMeta(fileSegment); + log.warn("FlatAppendFile last file size not correct, filePath: {}", this.filePath); + } + } + + public void initOffset(long offset) { + if (this.fileSegmentTable.isEmpty()) { + FileSegment fileSegment = fileSegmentFactory.createSegment(fileType, filePath, offset); + fileSegment.initPosition(fileSegment.getSize()); + this.flushFileSegmentMeta(fileSegment); + this.fileSegmentTable.add(fileSegment); + } + } + + public void flushFileSegmentMeta(FileSegment fileSegment) { + FileSegmentMetadata metadata = this.metadataStore.getFileSegment( + this.filePath, fileSegment.getFileType(), fileSegment.getBaseOffset()); + if (metadata == null) { + metadata = new FileSegmentMetadata( + this.filePath, fileSegment.getBaseOffset(), fileSegment.getFileType().getCode()); + metadata.setCreateTimestamp(System.currentTimeMillis()); + } + metadata.setSize(fileSegment.getCommitPosition()); + metadata.setBeginTimestamp(fileSegment.getMinTimestamp()); + metadata.setEndTimestamp(fileSegment.getMaxTimestamp()); + this.metadataStore.updateFileSegment(metadata); + } + + public String getFilePath() { + return filePath; + } + + public FileSegmentType getFileType() { + return fileType; + } + + public List getFileSegmentList() { + return fileSegmentTable; + } + + public long getMinOffset() { + List list = this.fileSegmentTable; + return list.isEmpty() ? 0L : list.get(0).getBaseOffset(); + } + + public long getCommitOffset() { + List list = this.fileSegmentTable; + return list.isEmpty() ? 0L : list.get(list.size() - 1).getCommitOffset(); + } + + public long getAppendOffset() { + List list = this.fileSegmentTable; + return list.isEmpty() ? 0L : list.get(list.size() - 1).getAppendOffset(); + } + + public long getMinTimestamp() { + List list = this.fileSegmentTable; + return list.isEmpty() ? GET_FILE_SIZE_ERROR : list.get(0).getMinTimestamp(); + } + + public long getMaxTimestamp() { + List list = this.fileSegmentTable; + return list.isEmpty() ? GET_FILE_SIZE_ERROR : list.get(list.size() - 1).getMaxTimestamp(); + } + + public FileSegment rollingNewFile(long offset) { + FileSegment fileSegment; + fileSegmentLock.writeLock().lock(); + try { + fileSegment = this.fileSegmentFactory.createSegment(this.fileType, this.filePath, offset); + this.flushFileSegmentMeta(fileSegment); + this.fileSegmentTable.add(fileSegment); + } finally { + fileSegmentLock.writeLock().unlock(); + } + return fileSegment; + } + + public FileSegment getFileToWrite() { + List fileSegmentList = this.fileSegmentTable; + if (fileSegmentList.isEmpty()) { + throw new IllegalStateException("Need to set base offset before create file segment"); + } else { + return fileSegmentList.get(fileSegmentList.size() - 1); + } + } + + public AppendResult append(ByteBuffer buffer, long timestamp) { + AppendResult result; + fileSegmentLock.writeLock().lock(); + try { + FileSegment fileSegment = this.getFileToWrite(); + result = fileSegment.append(buffer, timestamp); + if (result == AppendResult.FILE_FULL) { + boolean commitResult = fileSegment.commitAsync().join(); + log.info("FlatAppendFile#append not successful for the file {} is full, commit result={}", + fileSegment.getPath(), commitResult); + if (commitResult) { + this.flushFileSegmentMeta(fileSegment); + return this.rollingNewFile(this.getAppendOffset()).append(buffer, timestamp); + } else { + return AppendResult.UNKNOWN_ERROR; + } + } + } finally { + fileSegmentLock.writeLock().unlock(); + } + return result; + } + + public CompletableFuture commitAsync() { + List fileSegmentsList = this.fileSegmentTable; + if (fileSegmentsList.isEmpty()) { + return CompletableFuture.completedFuture(true); + } + FileSegment fileSegment = fileSegmentsList.get(fileSegmentsList.size() - 1); + return fileSegment.commitAsync().thenApply(success -> { + if (success) { + this.flushFileSegmentMeta(fileSegment); + } + return success; + }); + } + + public CompletableFuture readAsync(long offset, int length) { + List fileSegmentList = this.fileSegmentTable; + int index = fileSegmentList.size() - 1; + for (; index >= 0; index--) { + if (fileSegmentList.get(index).getBaseOffset() <= offset) { + break; + } + } + + FileSegment fileSegment1 = fileSegmentList.get(index); + FileSegment fileSegment2 = offset + length > fileSegment1.getCommitOffset() && + fileSegmentList.size() > index + 1 ? fileSegmentList.get(index + 1) : null; + + if (fileSegment2 == null) { + return fileSegment1.readAsync(offset - fileSegment1.getBaseOffset(), length); + } + + int segment1Length = (int) (fileSegment1.getCommitOffset() - offset); + return fileSegment1.readAsync(offset - fileSegment1.getBaseOffset(), segment1Length) + .thenCombine(fileSegment2.readAsync(0, length - segment1Length), + (buffer1, buffer2) -> { + ByteBuffer buffer = ByteBuffer.allocate(buffer1.remaining() + buffer2.remaining()); + buffer.put(buffer1).put(buffer2); + buffer.flip(); + return buffer; + }); + } + + public void shutdown() { + fileSegmentLock.writeLock().lock(); + try { + fileSegmentTable.forEach(FileSegment::close); + } finally { + fileSegmentLock.writeLock().unlock(); + } + } + + public void destroyExpiredFile(long expireTimestamp) { + fileSegmentLock.writeLock().lock(); + try { + while (!fileSegmentTable.isEmpty()) { + + // first remove expired file from fileSegmentTable + // then close and delete expired file + FileSegment fileSegment = fileSegmentTable.get(0); + + if (fileSegment.getMaxTimestamp() != Long.MAX_VALUE && + fileSegment.getMaxTimestamp() >= expireTimestamp) { + log.debug("FileSegment has not expired, filePath={}, fileType={}, " + + "offset={}, expireTimestamp={}, maxTimestamp={}", filePath, fileType, + fileSegment.getBaseOffset(), expireTimestamp, fileSegment.getMaxTimestamp()); + break; + } + + fileSegment.destroyFile(); + if (!fileSegment.exists()) { + fileSegmentTable.remove(0); + metadataStore.deleteFileSegment(filePath, fileType, fileSegment.getBaseOffset()); + } + } + } finally { + fileSegmentLock.writeLock().unlock(); + } + } + + public void destroy() { + this.destroyExpiredFile(Long.MAX_VALUE); + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/file/FlatCommitLogFile.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/file/FlatCommitLogFile.java new file mode 100644 index 0000000..bdf0bf3 --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/file/FlatCommitLogFile.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.tieredstore.file; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.tieredstore.common.FileSegmentType; +import org.apache.rocketmq.tieredstore.provider.FileSegment; +import org.apache.rocketmq.tieredstore.provider.FileSegmentFactory; +import org.apache.rocketmq.tieredstore.util.MessageFormatUtil; + +public class FlatCommitLogFile extends FlatAppendFile { + + private static final long GET_OFFSET_ERROR = -1L; + + private final AtomicLong firstOffset = new AtomicLong(GET_OFFSET_ERROR); + + public FlatCommitLogFile(FileSegmentFactory fileSegmentFactory, String filePath) { + super(fileSegmentFactory, FileSegmentType.COMMIT_LOG, filePath); + this.initOffset(0L); + } + + /** + * Two rules are set here: + * 1. Single file must be saved for more than one day as default. + * 2. Single file must reach the minimum size before switching. + * When calculating storage space, due to the limitation of condition 2, + * the actual usage of storage space may be slightly higher than expected. + */ + public boolean tryRollingFile(long interval) { + FileSegment fileSegment = this.getFileToWrite(); + long timestamp = fileSegment.getMinTimestamp(); + if (timestamp != Long.MAX_VALUE && timestamp + interval < System.currentTimeMillis() && + fileSegment.getAppendPosition() >= + fileSegmentFactory.getStoreConfig().getCommitLogRollingMinimumSize()) { + this.rollingNewFile(this.getAppendOffset()); + return true; + } + return false; + } + + public long getMinOffsetFromFile() { + return firstOffset.get() == GET_OFFSET_ERROR ? + this.getMinOffsetFromFileAsync().join() : firstOffset.get(); + } + + public CompletableFuture getMinOffsetFromFileAsync() { + int length = MessageFormatUtil.QUEUE_OFFSET_POSITION + Long.BYTES; + if (this.fileSegmentTable.isEmpty() || + this.getCommitOffset() - this.getMinOffset() < length) { + return CompletableFuture.completedFuture(GET_OFFSET_ERROR); + } + return this.readAsync(this.getMinOffset(), length) + .thenApply(buffer -> { + firstOffset.set(MessageFormatUtil.getQueueOffset(buffer)); + return firstOffset.get(); + }); + } + + @Override + public void destroyExpiredFile(long expireTimestamp) { + long beforeOffset = this.getMinOffset(); + super.destroyExpiredFile(expireTimestamp); + long afterOffset = this.getMinOffset(); + + if (beforeOffset != afterOffset && afterOffset > 0) { + log.info("CommitLog min cq offset reset, filePath={}, offset={}, expireTimestamp={}, change={}-{}", + filePath, firstOffset.get(), expireTimestamp, beforeOffset, afterOffset); + firstOffset.set(GET_OFFSET_ERROR); + } + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/file/FlatConsumeQueueFile.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/file/FlatConsumeQueueFile.java new file mode 100644 index 0000000..caad474 --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/file/FlatConsumeQueueFile.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.tieredstore.file; + +import org.apache.rocketmq.tieredstore.common.FileSegmentType; +import org.apache.rocketmq.tieredstore.provider.FileSegmentFactory; + +public class FlatConsumeQueueFile extends FlatAppendFile { + + public FlatConsumeQueueFile(FileSegmentFactory fileSegmentFactory, String filePath) { + super(fileSegmentFactory, FileSegmentType.CONSUME_QUEUE, filePath); + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/file/FlatFileFactory.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/file/FlatFileFactory.java new file mode 100644 index 0000000..d14ea7f --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/file/FlatFileFactory.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.tieredstore.file; + +import com.google.common.annotations.VisibleForTesting; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.MessageStoreExecutor; +import org.apache.rocketmq.tieredstore.common.FileSegmentType; +import org.apache.rocketmq.tieredstore.metadata.MetadataStore; +import org.apache.rocketmq.tieredstore.provider.FileSegmentFactory; + +public class FlatFileFactory { + + private final MetadataStore metadataStore; + private final MessageStoreConfig storeConfig; + private final FileSegmentFactory fileSegmentFactory; + + @VisibleForTesting + public FlatFileFactory(MetadataStore metadataStore, MessageStoreConfig storeConfig) { + this.metadataStore = metadataStore; + this.storeConfig = storeConfig; + this.fileSegmentFactory = new FileSegmentFactory(metadataStore, storeConfig, new MessageStoreExecutor()); + } + + public FlatFileFactory(MetadataStore metadataStore, + MessageStoreConfig storeConfig, MessageStoreExecutor executor) { + + this.metadataStore = metadataStore; + this.storeConfig = storeConfig; + this.fileSegmentFactory = new FileSegmentFactory(metadataStore, storeConfig, executor); + } + + public MessageStoreConfig getStoreConfig() { + return storeConfig; + } + + public MetadataStore getMetadataStore() { + return metadataStore; + } + + public FlatCommitLogFile createFlatFileForCommitLog(String filePath) { + return new FlatCommitLogFile(this.fileSegmentFactory, filePath); + } + + public FlatConsumeQueueFile createFlatFileForConsumeQueue(String filePath) { + return new FlatConsumeQueueFile(this.fileSegmentFactory, filePath); + } + + public FlatAppendFile createFlatFileForIndexFile(String filePath) { + return new FlatAppendFile(this.fileSegmentFactory, FileSegmentType.INDEX, filePath); + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/file/FlatFileInterface.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/file/FlatFileInterface.java new file mode 100644 index 0000000..01e7f25 --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/file/FlatFileInterface.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. + */ +package org.apache.rocketmq.tieredstore.file; + +import java.nio.ByteBuffer; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.locks.Lock; +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.tieredstore.common.AppendResult; + +public interface FlatFileInterface { + + long getTopicId(); + + Lock getFileLock(); + + MessageQueue getMessageQueue(); + + boolean isFlatFileInit(); + + void initOffset(long offset); + + boolean rollingFile(long interval); + + /** + * Appends a message to the commit log file + * + * @param message thByteBuffere message to append + * @return append result + */ + AppendResult appendCommitLog(ByteBuffer message); + + AppendResult appendCommitLog(SelectMappedBufferResult message); + + /** + * Append message to consume queue file, but does not commit it immediately + * + * @param request the dispatch request + * @return append result + */ + AppendResult appendConsumeQueue(DispatchRequest request); + + void release(); + + long getMinStoreTimestamp(); + + long getMaxStoreTimestamp(); + + long getFirstMessageOffset(); + + long getCommitLogMinOffset(); + + long getCommitLogMaxOffset(); + + long getCommitLogCommitOffset(); + + long getConsumeQueueMinOffset(); + + long getConsumeQueueMaxOffset(); + + long getConsumeQueueCommitOffset(); + + /** + * Persist commit log file and consume queue file + */ + CompletableFuture commitAsync(); + + /** + * Asynchronously retrieves the message at the specified consume queue offset + * + * @param consumeQueueOffset consume queue offset. + * @return the message inner object serialized content + */ + CompletableFuture getMessageAsync(long consumeQueueOffset); + + /** + * Get message from commitLog file at specified offset and length + * + * @param offset the offset + * @param length the length + * @return the message inner object serialized content + */ + CompletableFuture getCommitLogAsync(long offset, int length); + + /** + * Asynchronously retrieves the consume queue message at the specified queue offset + * + * @param consumeQueueOffset consume queue offset. + * @return the consumer queue unit serialized content + */ + CompletableFuture getConsumeQueueAsync(long consumeQueueOffset); + + /** + * Asynchronously reads the message body from the consume queue file at the specified offset and count + * + * @param consumeQueueOffset the message offset + * @param count the number of messages to read + * @return the consumer queue unit serialized content + */ + CompletableFuture getConsumeQueueAsync(long consumeQueueOffset, int count); + + /** + * Gets the start offset in the consume queue based on the timestamp and boundary type. + * The consume queues consist of ordered units, and their storage times are non-decreasing + * sequence. If the specified message exists, it returns the offset of either the first + * or last message, depending on the boundary type. If the specified message does not exist, + * it returns the offset of the next message as the pull offset. For example: + * ------------------------------------------------------------ + * store time : 40, 50, 50, 50, 60, 60, 70 + * queue offset : 10, 11, 12, 13, 14, 15, 16 + * ------------------------------------------------------------ + * query timestamp | boundary | result (reason) + * 35 | - | 10 (minimum offset) + * 45 | - | 11 (next offset) + * 50 | lower | 11 + * 50 | upper | 13 + * 60 | - | 14 (default to lower) + * 75 | - | 17 (maximum offset + 1) + * ------------------------------------------------------------ + * @param timestamp The search time + * @param boundaryType 'lower' or 'upper' to determine the boundary + * @return Returns the offset of the message in the consume queue + */ + CompletableFuture getQueueOffsetByTimeAsync(long timestamp, BoundaryType boundaryType); + + boolean isClosed(); + + /** + * Shutdown process + */ + void shutdown(); + + /** + * Destroys expired files + */ + void destroyExpiredFile(long timestamp); + + /** + * Delete file + */ + void destroy(); +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/file/FlatFileStore.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/file/FlatFileStore.java new file mode 100644 index 0000000..700f12d --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/file/FlatFileStore.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tieredstore.file; + +import com.google.common.base.Stopwatch; +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.MessageStoreExecutor; +import org.apache.rocketmq.tieredstore.metadata.MetadataStore; +import org.apache.rocketmq.tieredstore.metadata.entity.TopicMetadata; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FlatFileStore { + + private static final Logger log = LoggerFactory.getLogger(MessageStoreUtil.TIERED_STORE_LOGGER_NAME); + + private final MetadataStore metadataStore; + private final MessageStoreConfig storeConfig; + private final MessageStoreExecutor executor; + private final FlatFileFactory flatFileFactory; + private final ConcurrentMap flatFileConcurrentMap; + + public FlatFileStore(MessageStoreConfig storeConfig, MetadataStore metadataStore, MessageStoreExecutor executor) { + this.storeConfig = storeConfig; + this.metadataStore = metadataStore; + this.executor = executor; + this.flatFileFactory = new FlatFileFactory(metadataStore, storeConfig, executor); + this.flatFileConcurrentMap = new ConcurrentHashMap<>(); + } + + public boolean load() { + Stopwatch stopwatch = Stopwatch.createStarted(); + try { + this.flatFileConcurrentMap.clear(); + this.recover(); + log.info("FlatFileStore recover finished, total cost={}ms", stopwatch.elapsed(TimeUnit.MILLISECONDS)); + } catch (Exception e) { + long costTime = stopwatch.elapsed(TimeUnit.MILLISECONDS); + log.info("FlatFileStore recover error, total cost={}ms", costTime); + LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME) + .error("FlatFileStore recover error, total cost={}ms", costTime, e); + return false; + } + return true; + } + + public void recover() { + Semaphore semaphore = new Semaphore(storeConfig.getTieredStoreMaxPendingLimit() / 4); + List> futures = new ArrayList<>(); + metadataStore.iterateTopic(topicMetadata -> { + semaphore.acquireUninterruptibly(); + futures.add(this.recoverAsync(topicMetadata) + .whenComplete((unused, throwable) -> { + if (throwable != null) { + log.error("FlatFileStore recover file error, topic={}", topicMetadata.getTopic(), throwable); + } + semaphore.release(); + })); + }); + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + } + + public CompletableFuture recoverAsync(TopicMetadata topicMetadata) { + return CompletableFuture.runAsync(() -> { + Stopwatch stopwatch = Stopwatch.createStarted(); + AtomicLong queueCount = new AtomicLong(); + metadataStore.iterateQueue(topicMetadata.getTopic(), queueMetadata -> { + FlatMessageFile flatFile = this.computeIfAbsent(new MessageQueue( + topicMetadata.getTopic(), storeConfig.getBrokerName(), queueMetadata.getQueue().getQueueId())); + queueCount.incrementAndGet(); + log.debug("FlatFileStore recover file, topicId={}, topic={}, queueId={}, cost={}ms", + flatFile.getTopicId(), flatFile.getMessageQueue().getTopic(), + flatFile.getMessageQueue().getQueueId(), stopwatch.elapsed(TimeUnit.MILLISECONDS)); + }); + log.info("FlatFileStore recover file, topic={}, total={}, cost={}ms", + topicMetadata.getTopic(), queueCount.get(), stopwatch.elapsed(TimeUnit.MILLISECONDS)); + }, executor.bufferCommitExecutor); + } + + public void scheduleDeleteExpireFile() { + if (!storeConfig.isTieredStoreDeleteFileEnable()) { + return; + } + Stopwatch stopwatch = Stopwatch.createStarted(); + ImmutableList fileList = this.deepCopyFlatFileToList(); + for (FlatMessageFile flatFile : fileList) { + flatFile.getFileLock().lock(); + try { + flatFile.destroyExpiredFile(System.currentTimeMillis() - + TimeUnit.HOURS.toMillis(flatFile.getFileReservedHours())); + } catch (Exception e) { + log.error("FlatFileStore delete expire file error", e); + } finally { + flatFile.getFileLock().unlock(); + } + } + log.info("FlatFileStore schedule delete expired file, count={}, cost={}ms", + fileList.size(), stopwatch.elapsed(TimeUnit.MILLISECONDS)); + } + + public MetadataStore getMetadataStore() { + return metadataStore; + } + + public MessageStoreConfig getStoreConfig() { + return storeConfig; + } + + public FlatFileFactory getFlatFileFactory() { + return flatFileFactory; + } + + public FlatMessageFile computeIfAbsent(MessageQueue messageQueue) { + return flatFileConcurrentMap.computeIfAbsent(messageQueue, + mq -> new FlatMessageFile(flatFileFactory, mq.getTopic(), mq.getQueueId())); + } + + public FlatMessageFile getFlatFile(MessageQueue messageQueue) { + return flatFileConcurrentMap.get(messageQueue); + } + + public ImmutableList deepCopyFlatFileToList() { + return ImmutableList.copyOf(flatFileConcurrentMap.values()); + } + + public void shutdown() { + flatFileConcurrentMap.values().forEach(FlatMessageFile::shutdown); + } + + public void destroyFile(MessageQueue mq) { + if (mq == null) { + return; + } + + FlatMessageFile flatFile = flatFileConcurrentMap.remove(mq); + if (flatFile != null) { + flatFile.shutdown(); + flatFile.destroy(); + } + log.info("FlatFileStore destroy file, topic={}, queueId={}", mq.getTopic(), mq.getQueueId()); + } + + public void destroy() { + this.shutdown(); + flatFileConcurrentMap.values().forEach(FlatMessageFile::destroy); + flatFileConcurrentMap.clear(); + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/file/FlatMessageFile.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/file/FlatMessageFile.java new file mode 100644 index 0000000..b0e4dd6 --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/file/FlatMessageFile.java @@ -0,0 +1,426 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tieredstore.file; + +import com.alibaba.fastjson.JSON; +import com.google.common.annotations.VisibleForTesting; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Semaphore; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.common.AppendResult; +import org.apache.rocketmq.tieredstore.metadata.MetadataStore; +import org.apache.rocketmq.tieredstore.metadata.entity.QueueMetadata; +import org.apache.rocketmq.tieredstore.metadata.entity.TopicMetadata; +import org.apache.rocketmq.tieredstore.provider.FileSegment; +import org.apache.rocketmq.tieredstore.util.MessageFormatUtil; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FlatMessageFile implements FlatFileInterface { + + protected static final Logger log = LoggerFactory.getLogger(MessageStoreUtil.TIERED_STORE_LOGGER_NAME); + protected volatile boolean closed = false; + + protected TopicMetadata topicMetadata; + protected QueueMetadata queueMetadata; + + protected final String filePath; + protected final ReentrantLock fileLock; + protected final Semaphore commitLock = new Semaphore(1); + protected final MessageStoreConfig storeConfig; + protected final MetadataStore metadataStore; + protected final FlatCommitLogFile commitLog; + protected final FlatConsumeQueueFile consumeQueue; + + protected final ConcurrentMap> inFlightRequestMap; + + public FlatMessageFile(FlatFileFactory fileFactory, String topic, int queueId) { + this(fileFactory, MessageStoreUtil.toFilePath( + new MessageQueue(topic, fileFactory.getStoreConfig().getBrokerName(), queueId))); + this.topicMetadata = this.recoverTopicMetadata(topic); + this.queueMetadata = this.recoverQueueMetadata(topic, queueId); + } + + public FlatMessageFile(FlatFileFactory fileFactory, String filePath) { + this.filePath = filePath; + this.fileLock = new ReentrantLock(false); + this.storeConfig = fileFactory.getStoreConfig(); + this.metadataStore = fileFactory.getMetadataStore(); + this.commitLog = fileFactory.createFlatFileForCommitLog(filePath); + this.consumeQueue = fileFactory.createFlatFileForConsumeQueue(filePath); + this.inFlightRequestMap = new ConcurrentHashMap<>(); + } + + @Override + public long getTopicId() { + return topicMetadata.getTopicId(); + } + + @Override + public MessageQueue getMessageQueue() { + return queueMetadata != null ? queueMetadata.getQueue() : null; + } + + @Override + public boolean isFlatFileInit() { + return !this.consumeQueue.fileSegmentTable.isEmpty(); + } + + public TopicMetadata recoverTopicMetadata(String topic) { + TopicMetadata topicMetadata = this.metadataStore.getTopic(topic); + if (topicMetadata == null) { + topicMetadata = this.metadataStore.addTopic(topic, -1L); + } + return topicMetadata; + } + + public QueueMetadata recoverQueueMetadata(String topic, int queueId) { + MessageQueue mq = new MessageQueue(topic, storeConfig.getBrokerName(), queueId); + QueueMetadata queueMetadata = this.metadataStore.getQueue(mq); + if (queueMetadata == null) { + queueMetadata = this.metadataStore.addQueue(mq, -1L); + } + return queueMetadata; + } + + public void flushMetadata() { + if (queueMetadata != null) { + queueMetadata.setMinOffset(this.getConsumeQueueMinOffset()); + queueMetadata.setMaxOffset(this.getConsumeQueueCommitOffset()); + queueMetadata.setUpdateTimestamp(System.currentTimeMillis()); + metadataStore.updateQueue(queueMetadata); + } + } + + @Override + public Lock getFileLock() { + return this.fileLock; + } + + @VisibleForTesting + public Semaphore getCommitLock() { + return commitLock; + } + + @Override + public boolean rollingFile(long interval) { + return this.commitLog.tryRollingFile(interval); + } + + @Override + public void initOffset(long offset) { + fileLock.lock(); + try { + this.commitLog.initOffset(0L); + this.consumeQueue.initOffset(offset * MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE); + } finally { + fileLock.unlock(); + } + } + + @Override + public AppendResult appendCommitLog(ByteBuffer message) { + if (closed) { + return AppendResult.FILE_CLOSED; + } + return commitLog.append(message, MessageFormatUtil.getStoreTimeStamp(message)); + } + + @Override + public AppendResult appendCommitLog(SelectMappedBufferResult message) { + if (closed) { + return AppendResult.FILE_CLOSED; + } + return this.appendCommitLog(message.getByteBuffer()); + } + + @Override + public AppendResult appendConsumeQueue(DispatchRequest request) { + if (closed) { + return AppendResult.FILE_CLOSED; + } + + ByteBuffer buffer = ByteBuffer.allocate(MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE); + buffer.putLong(request.getCommitLogOffset()); + buffer.putInt(request.getMsgSize()); + buffer.putLong(request.getTagsCode()); + buffer.flip(); + + return consumeQueue.append(buffer, request.getStoreTimestamp()); + } + + @Override + public void release() { + } + + @Override + public long getMinStoreTimestamp() { + long minStoreTime = -1L; + if (Long.MAX_VALUE != commitLog.getMinTimestamp()) { + minStoreTime = Math.max(minStoreTime, commitLog.getMinTimestamp()); + } + if (Long.MAX_VALUE != consumeQueue.getMinTimestamp()) { + minStoreTime = Math.max(minStoreTime, consumeQueue.getMinTimestamp()); + } + return minStoreTime; + } + + @Override + public long getMaxStoreTimestamp() { + return commitLog.getMaxTimestamp(); + } + + @Override + public long getFirstMessageOffset() { + return commitLog.getMinOffsetFromFile(); + } + + @Override + public long getCommitLogMinOffset() { + return commitLog.getMinOffset(); + } + + @Override + public long getCommitLogMaxOffset() { + return commitLog.getAppendOffset(); + } + + @Override + public long getCommitLogCommitOffset() { + return commitLog.getCommitOffset(); + } + + @Override + public long getConsumeQueueMinOffset() { + long cqOffset = consumeQueue.getMinOffset() / MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE; + long effectiveOffset = this.commitLog.getMinOffsetFromFile(); + return Math.max(cqOffset, effectiveOffset); + } + + @Override + public long getConsumeQueueMaxOffset() { + return consumeQueue.getAppendOffset() / MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE; + } + + @Override + public long getConsumeQueueCommitOffset() { + return consumeQueue.getCommitOffset() / MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE; + } + + @Override + public CompletableFuture commitAsync() { + // acquire lock + if (commitLock.drainPermits() <= 0) { + return CompletableFuture.completedFuture(false); + } + + return this.commitLog.commitAsync() + .thenCompose(result -> { + if (result) { + return consumeQueue.commitAsync(); + } + return CompletableFuture.completedFuture(false); + }).whenComplete((result, throwable) -> commitLock.release()); + } + + @Override + public CompletableFuture getMessageAsync(long queueOffset) { + return getConsumeQueueAsync(queueOffset).thenCompose(cqBuffer -> { + long commitLogOffset = MessageFormatUtil.getCommitLogOffsetFromItem(cqBuffer); + int length = MessageFormatUtil.getSizeFromItem(cqBuffer); + return getCommitLogAsync(commitLogOffset, length); + }); + } + + @Override + public CompletableFuture getCommitLogAsync(long offset, int length) { + return commitLog.readAsync(offset, length); + } + + @Override + public CompletableFuture getConsumeQueueAsync(long queueOffset) { + return this.getConsumeQueueAsync(queueOffset, 1); + } + + @Override + public CompletableFuture getConsumeQueueAsync(long queueOffset, int count) { + return consumeQueue.readAsync( + queueOffset * MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE, + count * MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE); + } + + @Override + public CompletableFuture getQueueOffsetByTimeAsync(long timestamp, BoundaryType boundaryType) { + long cqMin = getConsumeQueueMinOffset(); + long cqMax = getConsumeQueueCommitOffset() - 1; + if (cqMax == -1 || cqMax < cqMin) { + return CompletableFuture.completedFuture(cqMin); + } + + ByteBuffer buffer = getMessageAsync(cqMax).join(); + long storeTime = MessageFormatUtil.getStoreTimeStamp(buffer); + if (storeTime < timestamp) { + log.info("FlatMessageFile getQueueOffsetByTimeAsync, exceeded maximum time, " + + "filePath={}, timestamp={}, result={}", filePath, timestamp, cqMax + 1); + return CompletableFuture.completedFuture(cqMax + 1); + } + + buffer = getMessageAsync(cqMin).join(); + storeTime = MessageFormatUtil.getStoreTimeStamp(buffer); + if (storeTime > timestamp) { + log.info("FlatMessageFile getQueueOffsetByTimeAsync, less than minimum time, " + + "filePath={}, timestamp={}, result={}", filePath, timestamp, cqMin); + return CompletableFuture.completedFuture(cqMin); + } + + // get correct consume queue file by binary search + List consumeQueueFileList = this.consumeQueue.getFileSegmentList(); + int low = 0, high = consumeQueueFileList.size() - 1; + int mid = low + (high - low) / 2; + while (low <= high) { + FileSegment fileSegment = consumeQueueFileList.get(mid); + if (fileSegment.getMinTimestamp() <= timestamp && timestamp <= fileSegment.getMaxTimestamp()) { + break; + } else if (timestamp < fileSegment.getMinTimestamp()) { + high = mid - 1; + } else { + low = mid + 1; + } + mid = low + (high - low) / 2; + } + FileSegment target = consumeQueueFileList.get(mid); + + // binary search lower bound index in a sorted array + long minOffset = target.getBaseOffset() / MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE; + long maxOffset = target.getCommitOffset() / MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE - 1; + List queryLog = new ArrayList<>(); + while (minOffset < maxOffset) { + long middle = minOffset + (maxOffset - minOffset) / 2; + buffer = this.getMessageAsync(middle).join(); + storeTime = MessageFormatUtil.getStoreTimeStamp(buffer); + queryLog.add(String.format("(range=%d-%d, middle=%d, timestamp=%d, diff=%dms)", + minOffset, maxOffset, middle, storeTime, timestamp - storeTime)); + if (storeTime < timestamp) { + minOffset = middle + 1; + } else { + maxOffset = middle; + } + } + + long offset = minOffset; + if (boundaryType == BoundaryType.UPPER) { + while (true) { + long next = offset + 1; + if (next > cqMax) { + break; + } + buffer = this.getMessageAsync(next).join(); + storeTime = MessageFormatUtil.getStoreTimeStamp(buffer); + if (storeTime == timestamp) { + offset = next; + } else { + break; + } + } + } + + log.info("FlatMessageFile getQueueOffsetByTimeAsync, filePath={}, timestamp={}, result={}, log={}", + filePath, timestamp, offset, JSON.toJSONString(queryLog)); + return CompletableFuture.completedFuture(offset); + } + + @Override + public int hashCode() { + return filePath.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + return StringUtils.equals(filePath, ((FlatMessageFile) obj).filePath); + } + + @Override + public boolean isClosed() { + return closed; + } + + @Override + public void shutdown() { + closed = true; + fileLock.lock(); + try { + consumeQueue.shutdown(); + commitLog.shutdown(); + } finally { + fileLock.unlock(); + } + } + + @Override + public void destroyExpiredFile(long timestamp) { + fileLock.lock(); + try { + consumeQueue.destroyExpiredFile(timestamp); + commitLog.destroyExpiredFile(timestamp); + } finally { + fileLock.unlock(); + } + } + + public void destroy() { + this.shutdown(); + fileLock.lock(); + try { + consumeQueue.destroyExpiredFile(Long.MAX_VALUE); + commitLog.destroyExpiredFile(Long.MAX_VALUE); + if (queueMetadata != null) { + metadataStore.deleteQueue(queueMetadata.getQueue()); + } + } finally { + fileLock.unlock(); + } + } + + public long getFileReservedHours() { + if (topicMetadata.getReserveTime() > 0) { + return topicMetadata.getReserveTime(); + } + return storeConfig.getTieredStoreFileReservedTime(); + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/index/IndexFile.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/index/IndexFile.java new file mode 100644 index 0000000..63d1193 --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/index/IndexFile.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.tieredstore.index; + +import java.nio.ByteBuffer; + +public interface IndexFile extends IndexService { + + /** + * Enumeration for the status of the index file. + */ + enum IndexStatusEnum { + SHUTDOWN, UNSEALED, SEALED, UPLOAD + } + + long getTimestamp(); + + long getEndTimestamp(); + + IndexStatusEnum getFileStatus(); + + ByteBuffer doCompaction(); +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/index/IndexItem.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/index/IndexItem.java new file mode 100644 index 0000000..24ccc43 --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/index/IndexItem.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.tieredstore.index; + +import java.nio.ByteBuffer; + +public class IndexItem { + + public static final int INDEX_ITEM_SIZE = 32; + public static final int COMPACT_INDEX_ITEM_SIZE = 28; + + private final int hashCode; + private final int topicId; + private final int queueId; + private final long offset; + private final int size; + private final int timeDiff; + private final int itemIndex; + + public IndexItem(int topicId, int queueId, long offset, int size, int hashCode, int timeDiff, int itemIndex) { + this.hashCode = hashCode; + this.topicId = topicId; + this.queueId = queueId; + this.offset = offset; + this.size = size; + this.timeDiff = timeDiff; + this.itemIndex = itemIndex; + } + + public IndexItem(byte[] bytes) { + if (bytes == null || + bytes.length != INDEX_ITEM_SIZE && + bytes.length != COMPACT_INDEX_ITEM_SIZE) { + throw new IllegalArgumentException("Byte array length not correct"); + } + + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + hashCode = byteBuffer.getInt(0); + topicId = byteBuffer.getInt(4); + queueId = byteBuffer.getInt(8); + offset = byteBuffer.getLong(12); + size = byteBuffer.getInt(20); + timeDiff = byteBuffer.getInt(24); + itemIndex = bytes.length == INDEX_ITEM_SIZE ? byteBuffer.getInt(28) : 0; + } + + public ByteBuffer getByteBuffer() { + ByteBuffer byteBuffer = ByteBuffer.allocate(32); + byteBuffer.putInt(0, hashCode); + byteBuffer.putInt(4, topicId); + byteBuffer.putInt(8, queueId); + byteBuffer.putLong(12, offset); + byteBuffer.putInt(20, size); + byteBuffer.putInt(24, timeDiff); + byteBuffer.putInt(28, itemIndex); + return byteBuffer; + } + + public int getHashCode() { + return hashCode; + } + + public int getTopicId() { + return topicId; + } + + public int getQueueId() { + return queueId; + } + + public long getOffset() { + return offset; + } + + public int getSize() { + return size; + } + + public int getTimeDiff() { + return timeDiff; + } + + public int getItemIndex() { + return itemIndex; + } + + @Override + public String toString() { + return "IndexItem{" + + "hashCode=" + hashCode + + ", topicId=" + topicId + + ", queueId=" + queueId + + ", offset=" + offset + + ", size=" + size + + ", timeDiff=" + timeDiff + + ", position=" + itemIndex + + '}'; + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/index/IndexService.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/index/IndexService.java new file mode 100644 index 0000000..a4ea7e7 --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/index/IndexService.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.tieredstore.index; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.tieredstore.common.AppendResult; + +public interface IndexService { + + void start(); + + /** + * Puts a key into the index. + * + * @param topic The topic of the key. + * @param topicId The ID of the topic. + * @param queueId The ID of the queue. + * @param keySet The set of keys to be indexed. + * @param offset The offset value of the key. + * @param size The size of the key. + * @param timestamp The timestamp of the key. + * @return The result of the put operation. + */ + AppendResult putKey( + String topic, int topicId, int queueId, Set keySet, long offset, int size, long timestamp); + + /** + * Asynchronously queries the index for a specific key within a given time range. + * + * @param topic The topic of the key. + * @param key The key to be queried. + * @param beginTime The start time of the query range. + * @param endTime The end time of the query range. + * @return A CompletableFuture that holds the list of IndexItems matching the query. + */ + CompletableFuture> queryAsync(String topic, String key, int maxCount, long beginTime, long endTime); + + default void forceUpload() { + } + + /** + * Shutdown the index service. + */ + void shutdown(); + + /** + * Destroys the index service and releases all resources. + */ + void destroy(); +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/index/IndexStoreFile.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/index/IndexStoreFile.java new file mode 100644 index 0000000..165c0b7 --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/index/IndexStoreFile.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.tieredstore.index; + +import com.google.common.base.Stopwatch; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.store.logfile.DefaultMappedFile; +import org.apache.rocketmq.store.logfile.MappedFile; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.common.AppendResult; +import org.apache.rocketmq.tieredstore.provider.FileSegment; +import org.apache.rocketmq.tieredstore.provider.PosixFileSegment; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.rocketmq.tieredstore.index.IndexFile.IndexStatusEnum.SEALED; +import static org.apache.rocketmq.tieredstore.index.IndexFile.IndexStatusEnum.UNSEALED; +import static org.apache.rocketmq.tieredstore.index.IndexFile.IndexStatusEnum.UPLOAD; +import static org.apache.rocketmq.tieredstore.index.IndexItem.COMPACT_INDEX_ITEM_SIZE; +import static org.apache.rocketmq.tieredstore.index.IndexStoreService.FILE_COMPACTED_DIRECTORY_NAME; +import static org.apache.rocketmq.tieredstore.index.IndexStoreService.FILE_DIRECTORY_NAME; + +/** + * a single IndexFile in indexService + */ +public class IndexStoreFile implements IndexFile { + + private static final Logger log = LoggerFactory.getLogger(MessageStoreUtil.TIERED_STORE_LOGGER_NAME); + + /** + * header format: + * magic code(4) + begin timestamp(8) + end timestamp(8) + slot num(4) + index num(4) + */ + public static final int INDEX_MAGIC_CODE = 0; + public static final int INDEX_BEGIN_TIME_STAMP = 4; + public static final int INDEX_END_TIME_STAMP = 12; + public static final int INDEX_SLOT_COUNT = 20; + public static final int INDEX_ITEM_INDEX = 24; + public static final int INDEX_HEADER_SIZE = 28; + + public static final int BEGIN_MAGIC_CODE = 0xCCDDEEFF ^ 1880681586 + 4; + public static final int END_MAGIC_CODE = 0xCCDDEEFF ^ 1880681586 + 8; + + /** + * hash slot + */ + private static final int INVALID_INDEX = 0; + private static final int HASH_SLOT_SIZE = Long.BYTES; + private static final int MAX_QUERY_COUNT = 512; + + private final int hashSlotMaxCount; + private final int indexItemMaxCount; + + private final ReadWriteLock fileReadWriteLock; + private final AtomicReference fileStatus; + private final AtomicLong beginTimestamp = new AtomicLong(-1L); + private final AtomicLong endTimestamp = new AtomicLong(-1L); + private final AtomicInteger hashSlotCount = new AtomicInteger(0); + private final AtomicInteger indexItemCount = new AtomicInteger(0); + + private MappedFile mappedFile; + private ByteBuffer byteBuffer; + private MappedFile compactMappedFile; + private FileSegment fileSegment; + + public IndexStoreFile(MessageStoreConfig storeConfig, long timestamp) throws IOException { + this.hashSlotMaxCount = storeConfig.getTieredStoreIndexFileMaxHashSlotNum(); + this.indexItemMaxCount = storeConfig.getTieredStoreIndexFileMaxIndexNum(); + this.fileStatus = new AtomicReference<>(UNSEALED); + this.fileReadWriteLock = new ReentrantReadWriteLock(); + this.mappedFile = new DefaultMappedFile( + Paths.get(storeConfig.getStorePathRootDir(), FILE_DIRECTORY_NAME, String.valueOf(timestamp)).toString(), + this.getItemPosition(indexItemMaxCount)); + this.byteBuffer = this.mappedFile.getMappedByteBuffer(); + + this.beginTimestamp.set(timestamp); + this.endTimestamp.set(byteBuffer.getLong(INDEX_BEGIN_TIME_STAMP)); + this.hashSlotCount.set(byteBuffer.getInt(INDEX_SLOT_COUNT)); + this.indexItemCount.set(byteBuffer.getInt(INDEX_ITEM_INDEX)); + this.flushNewMetadata(byteBuffer, indexItemMaxCount == this.indexItemCount.get() + 1); + } + + public IndexStoreFile(MessageStoreConfig storeConfig, FileSegment fileSegment) { + this.fileSegment = fileSegment; + this.fileStatus = new AtomicReference<>(UPLOAD); + this.fileReadWriteLock = new ReentrantReadWriteLock(); + + this.beginTimestamp.set(fileSegment.getMinTimestamp()); + this.endTimestamp.set(fileSegment.getMaxTimestamp()); + this.hashSlotCount.set(storeConfig.getTieredStoreIndexFileMaxHashSlotNum()); + this.indexItemCount.set(storeConfig.getTieredStoreIndexFileMaxIndexNum()); + this.hashSlotMaxCount = hashSlotCount.get(); + this.indexItemMaxCount = indexItemCount.get(); + } + + @Override + public long getTimestamp() { + return this.beginTimestamp.get(); + } + + @Override + public long getEndTimestamp() { + return this.endTimestamp.get(); + } + + public long getHashSlotCount() { + return this.hashSlotCount.get(); + } + + public long getIndexItemCount() { + return this.indexItemCount.get(); + } + + @Override + public IndexStatusEnum getFileStatus() { + return this.fileStatus.get(); + } + + protected String buildKey(String topic, String key) { + return String.format("%s#%s", topic, key); + } + + protected int hashCode(String keyStr) { + int keyHash = keyStr.hashCode(); + return (keyHash < 0) ? -keyHash : keyHash; + } + + protected void flushNewMetadata(ByteBuffer byteBuffer, boolean end) { + byteBuffer.putInt(INDEX_MAGIC_CODE, !end ? BEGIN_MAGIC_CODE : END_MAGIC_CODE); + byteBuffer.putLong(INDEX_BEGIN_TIME_STAMP, this.beginTimestamp.get()); + byteBuffer.putLong(INDEX_END_TIME_STAMP, this.endTimestamp.get()); + byteBuffer.putInt(INDEX_SLOT_COUNT, this.hashSlotCount.get()); + byteBuffer.putInt(INDEX_ITEM_INDEX, this.indexItemCount.get()); + } + + protected int getSlotPosition(int slotIndex) { + return INDEX_HEADER_SIZE + slotIndex * HASH_SLOT_SIZE; + } + + protected int getSlotValue(int slotPosition) { + return Math.max(this.byteBuffer.getInt(slotPosition), INVALID_INDEX); + } + + protected int getItemPosition(int itemIndex) { + return INDEX_HEADER_SIZE + hashSlotMaxCount * HASH_SLOT_SIZE + itemIndex * IndexItem.INDEX_ITEM_SIZE; + } + + @Override + public void start() { + + } + + @Override + public AppendResult putKey( + String topic, int topicId, int queueId, Set keySet, long offset, int size, long timestamp) { + + if (StringUtils.isBlank(topic)) { + return AppendResult.UNKNOWN_ERROR; + } + + if (keySet == null || keySet.isEmpty()) { + return AppendResult.SUCCESS; + } + + try { + fileReadWriteLock.writeLock().lock(); + + if (!UNSEALED.equals(fileStatus.get())) { + return AppendResult.FILE_FULL; + } + + if (this.indexItemCount.get() + keySet.size() >= this.indexItemMaxCount) { + this.fileStatus.set(IndexStatusEnum.SEALED); + return AppendResult.FILE_FULL; + } + + for (String key : keySet) { + int hashCode = this.hashCode(this.buildKey(topic, key)); + int slotPosition = this.getSlotPosition(hashCode % this.hashSlotMaxCount); + int slotOldValue = this.getSlotValue(slotPosition); + int timeDiff = (int) ((timestamp - this.beginTimestamp.get()) / 1000L); + + IndexItem indexItem = new IndexItem( + topicId, queueId, offset, size, hashCode, timeDiff, slotOldValue); + int itemIndex = this.indexItemCount.incrementAndGet(); + this.byteBuffer.position(this.getItemPosition(itemIndex)); + this.byteBuffer.put(indexItem.getByteBuffer()); + this.byteBuffer.putInt(slotPosition, itemIndex); + + if (slotOldValue <= INVALID_INDEX) { + this.hashSlotCount.incrementAndGet(); + } + if (this.endTimestamp.get() < timestamp) { + this.endTimestamp.set(timestamp); + } + this.flushNewMetadata(byteBuffer, indexItemMaxCount == this.indexItemCount.get() + 1); + + log.trace("IndexStoreFile put key, timestamp: {}, topic: {}, key: {}, slot: {}, item: {}, previous item: {}, content: {}", + this.getTimestamp(), topic, key, hashCode % this.hashSlotMaxCount, itemIndex, slotOldValue, indexItem); + } + return AppendResult.SUCCESS; + } catch (Exception e) { + log.error("IndexStoreFile put key error, topic: {}, topicId: {}, queueId: {}, keySet: {}, offset: {}, " + + "size: {}, timestamp: {}", topic, topicId, queueId, keySet, offset, size, timestamp, e); + } finally { + fileReadWriteLock.writeLock().unlock(); + } + + return AppendResult.UNKNOWN_ERROR; + } + + @Override + public CompletableFuture> queryAsync( + String topic, String key, int maxCount, long beginTime, long endTime) { + + switch (this.fileStatus.get()) { + case UNSEALED: + case SEALED: + return this.queryAsyncFromUnsealedFile(buildKey(topic, key), maxCount, beginTime, endTime); + case UPLOAD: + return this.queryAsyncFromSegmentFile(buildKey(topic, key), maxCount, beginTime, endTime); + case SHUTDOWN: + default: + return CompletableFuture.completedFuture(new ArrayList<>()); + } + } + + protected CompletableFuture> queryAsyncFromUnsealedFile( + String key, int maxCount, long beginTime, long endTime) { + + List result = new ArrayList<>(); + try { + fileReadWriteLock.readLock().lock(); + if (!UNSEALED.equals(this.fileStatus.get()) && !SEALED.equals(this.fileStatus.get())) { + return CompletableFuture.completedFuture(result); + } + + if (mappedFile == null || !mappedFile.hold()) { + return CompletableFuture.completedFuture(result); + } + + int hashCode = this.hashCode(key); + int slotPosition = this.getSlotPosition(hashCode % this.hashSlotMaxCount); + int slotValue = this.getSlotValue(slotPosition); + + int left = MAX_QUERY_COUNT; + while (left > 0 && + slotValue > INVALID_INDEX && + slotValue <= this.indexItemCount.get()) { + + byte[] bytes = new byte[IndexItem.INDEX_ITEM_SIZE]; + ByteBuffer buffer = this.byteBuffer.duplicate(); + buffer.position(this.getItemPosition(slotValue)); + buffer.get(bytes); + IndexItem indexItem = new IndexItem(bytes); + long storeTimestamp = indexItem.getTimeDiff() * 1000L + beginTimestamp.get(); + if (hashCode == indexItem.getHashCode() && + beginTime <= storeTimestamp && storeTimestamp <= endTime) { + result.add(indexItem); + if (result.size() > maxCount) { + break; + } + } + slotValue = indexItem.getItemIndex(); + left--; + } + + log.debug("IndexStoreFile query from unsealed mapped file, timestamp: {}, result size: {}, " + + "key: {}, hashCode: {}, maxCount: {}, timestamp={}-{}", + getTimestamp(), result.size(), key, hashCode, maxCount, beginTime, endTime); + } catch (Exception e) { + log.error("IndexStoreFile query from unsealed mapped file error, timestamp: {}, " + + "key: {}, maxCount: {}, timestamp={}-{}", getTimestamp(), key, maxCount, beginTime, endTime, e); + } finally { + fileReadWriteLock.readLock().unlock(); + mappedFile.release(); + } + + return CompletableFuture.completedFuture(result); + } + + protected CompletableFuture> queryAsyncFromSegmentFile( + String key, int maxCount, long beginTime, long endTime) { + + if (this.fileSegment == null || !UPLOAD.equals(this.fileStatus.get())) { + return CompletableFuture.completedFuture(Collections.emptyList()); + } + + Stopwatch stopwatch = Stopwatch.createStarted(); + int hashCode = this.hashCode(key); + int slotPosition = this.getSlotPosition(hashCode % this.hashSlotMaxCount); + + CompletableFuture> future = this.fileSegment.readAsync(slotPosition, HASH_SLOT_SIZE) + .thenCompose(slotBuffer -> { + if (slotBuffer.remaining() < HASH_SLOT_SIZE) { + log.error("IndexStoreFile query from tiered storage return error slot buffer, " + + "key: {}, maxCount: {}, timestamp={}-{}", key, maxCount, beginTime, endTime); + return CompletableFuture.completedFuture(null); + } + int indexPosition = slotBuffer.getInt(); + int indexTotalSize = Math.min(slotBuffer.getInt(), COMPACT_INDEX_ITEM_SIZE * 1024); + if (indexPosition <= INVALID_INDEX || indexTotalSize <= 0) { + return CompletableFuture.completedFuture(null); + } + return this.fileSegment.readAsync(indexPosition, indexTotalSize); + }) + .thenApply(itemBuffer -> { + List result = new ArrayList<>(); + if (itemBuffer == null) { + return result; + } + + if (itemBuffer.remaining() % COMPACT_INDEX_ITEM_SIZE != 0) { + log.error("IndexStoreFile query from tiered storage return error item buffer, " + + "key: {}, maxCount: {}, timestamp={}-{}", key, maxCount, beginTime, endTime); + return result; + } + + int size = itemBuffer.remaining() / COMPACT_INDEX_ITEM_SIZE; + byte[] bytes = new byte[COMPACT_INDEX_ITEM_SIZE]; + for (int i = 0; i < size; i++) { + itemBuffer.get(bytes); + IndexItem indexItem = new IndexItem(bytes); + long storeTimestamp = indexItem.getTimeDiff() * 1000L + beginTimestamp.get(); + if (hashCode == indexItem.getHashCode() && + beginTime <= storeTimestamp && storeTimestamp <= endTime && + result.size() < maxCount) { + result.add(indexItem); + } + } + return result; + }); + + return future.whenComplete((result, throwable) -> { + long costTime = stopwatch.elapsed(TimeUnit.MILLISECONDS); + if (throwable != null) { + log.error("IndexStoreFile query from segment file, cost: {}ms, timestamp: {}, " + + "key: {}, hashCode: {}, maxCount: {}, timestamp={}-{}", + costTime, getTimestamp(), key, hashCode, maxCount, beginTime, endTime, throwable); + } else { + String details = Optional.ofNullable(result) + .map(r -> r.stream() + .map(item -> String.format("%d-%d", item.getQueueId(), item.getOffset())) + .collect(Collectors.joining(", "))) + .orElse(""); + + log.debug("IndexStoreFile query from segment file, cost: {}ms, timestamp: {}, result size: {}, ({}), " + + "key: {}, hashCode: {}, maxCount: {}, timestamp={}-{}", + costTime, getTimestamp(), result != null ? result.size() : 0, details, key, hashCode, maxCount, beginTime, endTime); + } + }); + } + + @Override + public ByteBuffer doCompaction() { + Stopwatch stopwatch = Stopwatch.createStarted(); + ByteBuffer buffer; + try { + buffer = compactToNewFile(); + log.debug("IndexStoreFile do compaction, timestamp: {}, file size: {}, cost: {}ms", + this.getTimestamp(), buffer.capacity(), stopwatch.elapsed(TimeUnit.MICROSECONDS)); + } catch (Exception e) { + log.error("IndexStoreFile do compaction, timestamp: {}, cost: {}ms", + this.getTimestamp(), stopwatch.elapsed(TimeUnit.MICROSECONDS), e); + return null; + } + + try { + // Make sure there is no read request here + fileReadWriteLock.writeLock().lock(); + fileStatus.set(IndexStatusEnum.SEALED); + } catch (Exception e) { + log.error("IndexStoreFile change file status to sealed error, timestamp={}", this.getTimestamp()); + } finally { + fileReadWriteLock.writeLock().unlock(); + } + return buffer; + } + + protected String getCompactedFilePath() { + return Paths.get(this.mappedFile.getFileName()).getParent() + .resolve(FILE_COMPACTED_DIRECTORY_NAME) + .resolve(String.valueOf(this.getTimestamp())).toString(); + } + + protected ByteBuffer compactToNewFile() throws IOException { + + byte[] payload = new byte[IndexItem.INDEX_ITEM_SIZE]; + ByteBuffer payloadBuffer = ByteBuffer.wrap(payload); + int writePosition = INDEX_HEADER_SIZE + (hashSlotMaxCount * HASH_SLOT_SIZE); + int fileMaxLength = writePosition + COMPACT_INDEX_ITEM_SIZE * indexItemCount.get(); + + compactMappedFile = new DefaultMappedFile(this.getCompactedFilePath(), fileMaxLength); + MappedByteBuffer newBuffer = compactMappedFile.getMappedByteBuffer(); + + for (int i = 0; i < hashSlotMaxCount; i++) { + int slotPosition = this.getSlotPosition(i); + int slotValue = this.getSlotValue(slotPosition); + int writeBeginPosition = writePosition; + + while (slotValue > INVALID_INDEX && writePosition < fileMaxLength) { + ByteBuffer buffer = this.byteBuffer.duplicate(); + buffer.position(this.getItemPosition(slotValue)); + buffer.get(payload); + int newSlotValue = payloadBuffer.getInt(COMPACT_INDEX_ITEM_SIZE); + buffer.limit(COMPACT_INDEX_ITEM_SIZE); + newBuffer.position(writePosition); + newBuffer.put(payload, 0, COMPACT_INDEX_ITEM_SIZE); + log.trace("IndexStoreFile do compaction, write item, slot: {}, current: {}, next: {}", i, slotValue, newSlotValue); + slotValue = newSlotValue; + writePosition += COMPACT_INDEX_ITEM_SIZE; + } + + int length = writePosition - writeBeginPosition; + newBuffer.putInt(slotPosition, writeBeginPosition); + newBuffer.putInt(slotPosition + Integer.BYTES, length); + + if (length > 0) { + log.trace("IndexStoreFile do compaction, write slot, slot: {}, begin: {}, length: {}", i, writeBeginPosition, length); + } + } + + this.flushNewMetadata(newBuffer, true); + newBuffer.flip(); + return newBuffer; + } + + @Override + public void shutdown() { + try { + fileReadWriteLock.writeLock().lock(); + this.fileStatus.set(IndexStatusEnum.SHUTDOWN); + if (this.fileSegment != null && this.fileSegment instanceof PosixFileSegment) { + this.fileSegment.close(); + } + if (this.mappedFile != null) { + this.mappedFile.shutdown(TimeUnit.SECONDS.toMillis(10)); + } + if (this.compactMappedFile != null) { + this.compactMappedFile.shutdown(TimeUnit.SECONDS.toMillis(10)); + } + } catch (Exception e) { + log.error("IndexStoreFile shutdown failed, timestamp: {}, status: {}", this.getTimestamp(), fileStatus.get(), e); + } finally { + fileReadWriteLock.writeLock().unlock(); + } + } + + @Override + public void destroy() { + try { + fileReadWriteLock.writeLock().lock(); + this.shutdown(); + switch (this.fileStatus.get()) { + case SHUTDOWN: + case UNSEALED: + case SEALED: + if (this.mappedFile != null) { + this.mappedFile.destroy(TimeUnit.SECONDS.toMillis(10)); + } + if (this.compactMappedFile != null) { + this.compactMappedFile.destroy(TimeUnit.SECONDS.toMillis(10)); + } + log.debug("IndexStoreService destroy local file, timestamp: {}, status: {}", this.getTimestamp(), fileStatus.get()); + break; + case UPLOAD: + log.warn("[BUG] IndexStoreService destroy remote file, timestamp: {}", this.getTimestamp()); + } + } catch (Exception e) { + log.error("IndexStoreService destroy failed, timestamp: {}, status: {}", this.getTimestamp(), fileStatus.get(), e); + } finally { + fileReadWriteLock.writeLock().unlock(); + } + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/index/IndexStoreService.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/index/IndexStoreService.java new file mode 100644 index 0000000..f4f602a --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/index/IndexStoreService.java @@ -0,0 +1,444 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tieredstore.index; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Stopwatch; +import java.io.File; +import java.nio.ByteBuffer; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +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.ConcurrentNavigableMap; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.store.logfile.DefaultMappedFile; +import org.apache.rocketmq.store.logfile.MappedFile; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.common.AppendResult; +import org.apache.rocketmq.tieredstore.file.FlatAppendFile; +import org.apache.rocketmq.tieredstore.file.FlatFileFactory; +import org.apache.rocketmq.tieredstore.provider.FileSegment; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class IndexStoreService extends ServiceThread implements IndexService { + + private static final Logger log = LoggerFactory.getLogger(MessageStoreUtil.TIERED_STORE_LOGGER_NAME); + + public static final String FILE_DIRECTORY_NAME = "tiered_index_file"; + public static final String FILE_COMPACTED_DIRECTORY_NAME = "compacting"; + + /** + * File status in table example: + * upload, upload, upload, sealed, sealed, unsealed + */ + private final MessageStoreConfig storeConfig; + private final ConcurrentSkipListMap timeStoreTable; + private final ReadWriteLock readWriteLock; + private final AtomicLong compactTimestamp; + private final String filePath; + private final FlatFileFactory fileAllocator; + private final boolean autoCreateNewFile; + + private volatile IndexFile currentWriteFile; + private volatile FlatAppendFile flatAppendFile; + + public IndexStoreService(FlatFileFactory flatFileFactory, String filePath) { + this(flatFileFactory, filePath, true); + } + + public IndexStoreService(FlatFileFactory flatFileFactory, String filePath, boolean autoCreateNewFile) { + this.storeConfig = flatFileFactory.getStoreConfig(); + this.filePath = filePath; + this.fileAllocator = flatFileFactory; + this.timeStoreTable = new ConcurrentSkipListMap<>(); + this.compactTimestamp = new AtomicLong(0L); + this.readWriteLock = new ReentrantReadWriteLock(); + this.autoCreateNewFile = autoCreateNewFile; + } + + @Override + public void start() { + this.recover(); + super.start(); + } + + private void doConvertOldFormatFile(String filePath) { + try { + File file = new File(filePath); + if (!file.exists()) { + return; + } + MappedFile mappedFile = new DefaultMappedFile(file.getPath(), (int) file.length()); + long timestamp = mappedFile.getMappedByteBuffer().getLong(IndexStoreFile.INDEX_BEGIN_TIME_STAMP); + if (timestamp <= 0) { + mappedFile.destroy(TimeUnit.SECONDS.toMillis(10)); + } else { + mappedFile.renameTo(String.valueOf(new File(file.getParent(), String.valueOf(timestamp)))); + mappedFile.shutdown(TimeUnit.SECONDS.toMillis(10)); + } + } catch (Exception e) { + log.error("IndexStoreService do convert old format error, file: {}", filePath, e); + } + } + + private void recover() { + Stopwatch stopwatch = Stopwatch.createStarted(); + + // delete compact file directory + UtilAll.deleteFile(new File(Paths.get(storeConfig.getStorePathRootDir(), + FILE_DIRECTORY_NAME, FILE_COMPACTED_DIRECTORY_NAME).toString())); + + // recover local + File dir = new File(Paths.get(storeConfig.getStorePathRootDir(), FILE_DIRECTORY_NAME).toString()); + this.doConvertOldFormatFile(Paths.get(dir.getPath(), "0000").toString()); + this.doConvertOldFormatFile(Paths.get(dir.getPath(), "1111").toString()); + File[] files = dir.listFiles(); + + if (files != null) { + List fileList = Arrays.asList(files); + fileList.sort(Comparator.comparing(File::getName)); + + for (File file : fileList) { + if (file.isDirectory() || !StringUtils.isNumeric(file.getName())) { + continue; + } + + try { + IndexFile indexFile = new IndexStoreFile(storeConfig, Long.parseLong(file.getName())); + timeStoreTable.put(indexFile.getTimestamp(), indexFile); + log.info("IndexStoreService recover load local file, timestamp: {}", indexFile.getTimestamp()); + } catch (Exception e) { + log.error("IndexStoreService recover, load local file error", e); + } + } + } + + if (this.autoCreateNewFile && this.timeStoreTable.isEmpty()) { + this.createNewIndexFile(System.currentTimeMillis()); + } + + if (!this.timeStoreTable.isEmpty()) { + this.currentWriteFile = this.timeStoreTable.lastEntry().getValue(); + this.setCompactTimestamp(this.timeStoreTable.firstKey() - 1); + } + + // recover remote + this.flatAppendFile = fileAllocator.createFlatFileForIndexFile(filePath); + + for (FileSegment fileSegment : flatAppendFile.getFileSegmentList()) { + IndexFile indexFile = new IndexStoreFile(storeConfig, fileSegment); + IndexFile localFile = timeStoreTable.get(indexFile.getTimestamp()); + if (localFile != null) { + localFile.destroy(); + } + timeStoreTable.put(indexFile.getTimestamp(), indexFile); + log.info("IndexStoreService recover load remote file, timestamp: {}, end timestamp: {}", + indexFile.getTimestamp(), indexFile.getEndTimestamp()); + } + + log.info("IndexStoreService recover finished, total: {}, cost: {}ms, directory: {}", + timeStoreTable.size(), stopwatch.elapsed(TimeUnit.MILLISECONDS), dir.getAbsolutePath()); + } + + public void createNewIndexFile(long timestamp) { + try { + this.readWriteLock.writeLock().lock(); + IndexFile indexFile = this.currentWriteFile; + if (this.timeStoreTable.containsKey(timestamp) || + indexFile != null && IndexFile.IndexStatusEnum.UNSEALED.equals(indexFile.getFileStatus())) { + return; + } + IndexStoreFile newStoreFile = new IndexStoreFile(storeConfig, timestamp); + this.timeStoreTable.put(timestamp, newStoreFile); + this.currentWriteFile = newStoreFile; + log.info("IndexStoreService construct next file, timestamp: {}", timestamp); + } catch (Exception e) { + log.error("IndexStoreService construct next file, timestamp: {}", timestamp, e); + } finally { + this.readWriteLock.writeLock().unlock(); + } + } + + @VisibleForTesting + public ConcurrentSkipListMap getTimeStoreTable() { + return timeStoreTable; + } + + @Override + public AppendResult putKey( + String topic, int topicId, int queueId, Set keySet, long offset, int size, long timestamp) { + + if (StringUtils.isBlank(topic)) { + return AppendResult.UNKNOWN_ERROR; + } + + if (keySet == null || keySet.isEmpty()) { + return AppendResult.SUCCESS; + } + + for (int i = 0; i < 3; i++) { + AppendResult result = this.currentWriteFile.putKey( + topic, topicId, queueId, keySet, offset, size, timestamp); + + if (AppendResult.SUCCESS.equals(result)) { + return AppendResult.SUCCESS; + } else if (AppendResult.FILE_FULL.equals(result)) { + // use current time to ensure the order of file + this.createNewIndexFile(System.currentTimeMillis()); + } + } + + log.error("IndexStoreService put key three times return error, topic: {}, topicId: {}, " + + "queueId: {}, keySize: {}, timestamp: {}", topic, topicId, queueId, keySet.size(), timestamp); + return AppendResult.SUCCESS; + } + + @Override + public CompletableFuture> queryAsync( + String topic, String key, int maxCount, long beginTime, long endTime) { + + if (beginTime > endTime) { + return CompletableFuture.completedFuture(new ArrayList<>()); + } + + CompletableFuture> future = new CompletableFuture<>(); + try { + readWriteLock.readLock().lock(); + ConcurrentNavigableMap pendingMap = + this.timeStoreTable.subMap(beginTime, true, endTime, true); + List> futureList = new ArrayList<>(pendingMap.size()); + ConcurrentHashMap result = new ConcurrentHashMap<>(); + + for (Map.Entry entry : pendingMap.descendingMap().entrySet()) { + CompletableFuture completableFuture = entry.getValue() + .queryAsync(topic, key, maxCount, beginTime, endTime) + .thenAccept(itemList -> itemList.forEach(indexItem -> { + if (result.size() < maxCount) { + result.put(String.format( + "%d-%d", indexItem.getQueueId(), indexItem.getOffset()), indexItem); + } + })); + futureList.add(completableFuture); + } + + CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])) + .whenComplete((v, t) -> { + // Try to return the query results as much as possible here + // rather than directly throwing exceptions + if (t != null) { + log.error("IndexStoreService#queryAsync, topicId={}, key={}, maxCount={}, timestamp={}-{}", + topic, key, maxCount, beginTime, endTime, t); + } + List resultList = new ArrayList<>(result.values()); + future.complete(resultList.subList(0, Math.min(resultList.size(), maxCount))); + }); + } catch (Exception e) { + log.error("IndexStoreService#queryAsync, topicId={}, key={}, maxCount={}, timestamp={}-{}", + topic, key, maxCount, beginTime, endTime, e); + future.completeExceptionally(e); + } finally { + readWriteLock.readLock().unlock(); + } + return future; + } + + @Override + public void forceUpload() { + try { + readWriteLock.writeLock().lock(); + while (true) { + Map.Entry entry = + this.timeStoreTable.higherEntry(this.compactTimestamp.get()); + if (entry == null) { + break; + } + if (this.doCompactThenUploadFile(entry.getValue())) { + this.setCompactTimestamp(entry.getValue().getTimestamp()); + // The total number of files will not too much, prevent io too fast. + TimeUnit.MILLISECONDS.sleep(50); + } + } + } catch (Exception e) { + log.error("IndexStoreService force upload error", e); + throw new RuntimeException(e); + } finally { + readWriteLock.writeLock().unlock(); + } + } + + public boolean doCompactThenUploadFile(IndexFile indexFile) { + if (IndexFile.IndexStatusEnum.UPLOAD.equals(indexFile.getFileStatus())) { + log.error("IndexStoreService file status not correct, so skip, timestamp: {}, status: {}", + indexFile.getTimestamp(), indexFile.getFileStatus()); + indexFile.destroy(); + return true; + } + + Stopwatch stopwatch = Stopwatch.createStarted(); + if (flatAppendFile.getCommitOffset() == flatAppendFile.getAppendOffset()) { + ByteBuffer byteBuffer = indexFile.doCompaction(); + if (byteBuffer == null) { + log.error("IndexStoreService found compaction buffer is null, timestamp: {}", indexFile.getTimestamp()); + return false; + } + flatAppendFile.rollingNewFile(Math.max(0L, flatAppendFile.getAppendOffset())); + flatAppendFile.append(byteBuffer, indexFile.getTimestamp()); + flatAppendFile.getFileToWrite().setMinTimestamp(indexFile.getTimestamp()); + flatAppendFile.getFileToWrite().setMaxTimestamp(indexFile.getEndTimestamp()); + } + boolean result = flatAppendFile.commitAsync().join(); + + List fileSegmentList = flatAppendFile.getFileSegmentList(); + FileSegment fileSegment = fileSegmentList.get(fileSegmentList.size() - 1); + if (!result || fileSegment == null || fileSegment.getMinTimestamp() != indexFile.getTimestamp()) { + log.warn("IndexStoreService upload compacted file error, timestamp: {}", indexFile.getTimestamp()); + return false; + } else { + log.info("IndexStoreService upload compacted file success, timestamp: {}", indexFile.getTimestamp()); + } + + readWriteLock.writeLock().lock(); + try { + IndexFile storeFile = new IndexStoreFile(storeConfig, fileSegment); + timeStoreTable.put(storeFile.getTimestamp(), storeFile); + indexFile.destroy(); + } catch (Exception e) { + log.error("IndexStoreService rolling file error, timestamp: {}, cost: {}ms", + indexFile.getTimestamp(), stopwatch.elapsed(TimeUnit.MILLISECONDS), e); + } finally { + readWriteLock.writeLock().unlock(); + } + return true; + } + + public void destroyExpiredFile(long expireTimestamp) { + // delete file in time store table + readWriteLock.writeLock().lock(); + try { + flatAppendFile.destroyExpiredFile(expireTimestamp); + timeStoreTable.entrySet().removeIf(entry -> + IndexFile.IndexStatusEnum.UPLOAD.equals(entry.getValue().getFileStatus()) && + entry.getKey() < flatAppendFile.getMinTimestamp()); + int tableSize = (int) timeStoreTable.entrySet().stream() + .filter(entry -> IndexFile.IndexStatusEnum.UPLOAD.equals(entry.getValue().getFileStatus())) + .count(); + log.info("IndexStoreService delete file, timestamp={}, remote={}, table={}, all={}", + expireTimestamp, flatAppendFile.getFileSegmentList().size(), tableSize, timeStoreTable.size()); + } finally { + readWriteLock.writeLock().unlock(); + } + } + + public void destroy() { + readWriteLock.writeLock().lock(); + try { + // delete local store file + for (Map.Entry entry : timeStoreTable.entrySet()) { + IndexFile indexFile = entry.getValue(); + if (IndexFile.IndexStatusEnum.UPLOAD.equals(indexFile.getFileStatus())) { + continue; + } + indexFile.destroy(); + } + // delete remote + if (flatAppendFile != null) { + flatAppendFile.destroy(); + } + } catch (Exception e) { + log.error("IndexStoreService destroy all file error", e); + } finally { + readWriteLock.writeLock().unlock(); + } + } + + @Override + public String getServiceName() { + return IndexStoreService.class.getSimpleName(); + } + + public void setCompactTimestamp(long timestamp) { + this.compactTimestamp.set(timestamp); + log.debug("IndexStoreService set compact timestamp to: {}", timestamp); + } + + protected IndexFile getNextSealedFile() { + Map.Entry entry = + this.timeStoreTable.higherEntry(this.compactTimestamp.get()); + if (entry != null && entry.getKey() < this.timeStoreTable.lastKey()) { + return entry.getValue(); + } + return null; + } + + @Override + public void shutdown() { + super.shutdown(); + // Wait index service upload then clear time store table + while (!this.timeStoreTable.isEmpty()) { + try { + TimeUnit.MILLISECONDS.sleep(50); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + @Override + public void run() { + while (!this.isStopped()) { + long expireTimestamp = System.currentTimeMillis() + - TimeUnit.HOURS.toMillis(storeConfig.getTieredStoreFileReservedTime()); + this.destroyExpiredFile(expireTimestamp); + IndexFile indexFile = this.getNextSealedFile(); + if (indexFile != null) { + if (this.doCompactThenUploadFile(indexFile)) { + this.setCompactTimestamp(indexFile.getTimestamp()); + continue; + } + } + this.waitForRunning(TimeUnit.SECONDS.toMillis(10)); + } + readWriteLock.writeLock().lock(); + try { + if (autoCreateNewFile) { + this.forceUpload(); + } + this.timeStoreTable.forEach((timestamp, file) -> file.shutdown()); + this.timeStoreTable.clear(); + } catch (Exception e) { + log.error("IndexStoreService shutdown error", e); + } finally { + readWriteLock.writeLock().unlock(); + } + log.info(this.getServiceName() + " service shutdown"); + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/metadata/DefaultMetadataStore.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/metadata/DefaultMetadataStore.java new file mode 100644 index 0000000..09500bf --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/metadata/DefaultMetadataStore.java @@ -0,0 +1,359 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tieredstore.metadata; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.serializer.SerializerFeature; +import com.google.common.annotations.VisibleForTesting; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; +import org.apache.rocketmq.common.ConfigManager; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.common.FileSegmentType; +import org.apache.rocketmq.tieredstore.metadata.entity.FileSegmentMetadata; +import org.apache.rocketmq.tieredstore.metadata.entity.QueueMetadata; +import org.apache.rocketmq.tieredstore.metadata.entity.TopicMetadata; + +public class DefaultMetadataStore extends ConfigManager implements MetadataStore { + + private static final int DEFAULT_CAPACITY = 1024; + private static final String DEFAULT_CONFIG_NAME = "config"; + private static final String DEFAULT_FILE_NAME = "tieredStoreMetadata.json"; + + private final AtomicLong topicSequenceNumber; + private final MessageStoreConfig storeConfig; + private final ConcurrentMap topicMetadataTable; + private final ConcurrentMap> queueMetadataTable; + + // Declare concurrent mapping tables to store file segment metadata + // Key: filePath -> Value: + private final ConcurrentMap> commitLogFileSegmentTable; + private final ConcurrentMap> consumeQueueFileSegmentTable; + private final ConcurrentMap> indexFileSegmentTable; + + public DefaultMetadataStore(MessageStoreConfig storeConfig) { + this.storeConfig = storeConfig; + this.topicSequenceNumber = new AtomicLong(-1L); + this.topicMetadataTable = new ConcurrentHashMap<>(DEFAULT_CAPACITY); + this.queueMetadataTable = new ConcurrentHashMap<>(DEFAULT_CAPACITY); + this.commitLogFileSegmentTable = new ConcurrentHashMap<>(DEFAULT_CAPACITY); + this.consumeQueueFileSegmentTable = new ConcurrentHashMap<>(DEFAULT_CAPACITY); + this.indexFileSegmentTable = new ConcurrentHashMap<>(DEFAULT_CAPACITY); + this.load(); + } + + @Override + public String encode() { + return this.encode(false); + } + + @Override + public String encode(boolean prettyFormat) { + TieredMetadataSerializeWrapper dataWrapper = new TieredMetadataSerializeWrapper(); + dataWrapper.setTopicSerialNumber(topicSequenceNumber); + dataWrapper.setTopicMetadataTable(topicMetadataTable); + dataWrapper.setQueueMetadataTable(new ConcurrentHashMap<>(queueMetadataTable)); + dataWrapper.setCommitLogFileSegmentTable(new ConcurrentHashMap<>(commitLogFileSegmentTable)); + dataWrapper.setConsumeQueueFileSegmentTable(new ConcurrentHashMap<>(consumeQueueFileSegmentTable)); + dataWrapper.setIndexFileSegmentTable(new ConcurrentHashMap<>(indexFileSegmentTable)); + + if (prettyFormat) { + return JSON.toJSONString( + dataWrapper, SerializerFeature.DisableCircularReferenceDetect, SerializerFeature.PrettyFormat); + } else { + return JSON.toJSONString( + dataWrapper, SerializerFeature.DisableCircularReferenceDetect); + } + } + + @Override + public String configFilePath() { + return Paths.get(storeConfig.getStorePathRootDir(), DEFAULT_CONFIG_NAME, DEFAULT_FILE_NAME).toString(); + } + + @Override + public boolean load() { + return super.load(); + } + + @Override + public void decode(String jsonString) { + if (jsonString != null) { + TieredMetadataSerializeWrapper dataWrapper = + TieredMetadataSerializeWrapper.fromJson(jsonString, TieredMetadataSerializeWrapper.class); + if (dataWrapper != null) { + this.topicSequenceNumber.set(dataWrapper.getTopicSerialNumber().get()); + this.topicMetadataTable.putAll(dataWrapper.getTopicMetadataTable()); + dataWrapper.getQueueMetadataTable().forEach( + (topic, entry) -> this.queueMetadataTable.put(topic, new ConcurrentHashMap<>(entry))); + dataWrapper.getCommitLogFileSegmentTable().forEach( + (filePath, entry) -> this.commitLogFileSegmentTable.put(filePath, new ConcurrentHashMap<>(entry))); + dataWrapper.getConsumeQueueFileSegmentTable().forEach( + (filePath, entry) -> this.consumeQueueFileSegmentTable.put(filePath, new ConcurrentHashMap<>(entry))); + dataWrapper.getIndexFileSegmentTable().forEach( + (filePath, entry) -> this.indexFileSegmentTable.put(filePath, new ConcurrentHashMap<>(entry))); + } + } + } + + @Override + public TopicMetadata getTopic(String topic) { + return topicMetadataTable.get(topic); + } + + @Override + public void iterateTopic(Consumer callback) { + topicMetadataTable.values().forEach(callback); + } + + @Override + public TopicMetadata addTopic(String topic, long reserveTime) { + TopicMetadata old = getTopic(topic); + if (old != null) { + return old; + } + TopicMetadata metadata = new TopicMetadata(topicSequenceNumber.incrementAndGet(), topic, reserveTime); + topicMetadataTable.put(topic, metadata); + persist(); + return metadata; + } + + @Override + public void updateTopic(TopicMetadata topicMetadata) { + TopicMetadata metadata = getTopic(topicMetadata.getTopic()); + if (metadata == null) { + return; + } + metadata.setUpdateTimestamp(System.currentTimeMillis()); + topicMetadataTable.put(topicMetadata.getTopic(), topicMetadata); + persist(); + } + + @Override + public void deleteTopic(String topic) { + topicMetadataTable.remove(topic); + persist(); + } + + @Override + public QueueMetadata getQueue(MessageQueue mq) { + return queueMetadataTable.getOrDefault(mq.getTopic(), new ConcurrentHashMap<>()).get(mq.getQueueId()); + } + + @Override + public void iterateQueue(String topic, Consumer callback) { + ConcurrentMap metadataConcurrentMap = queueMetadataTable.get(topic); + if (metadataConcurrentMap != null) { + metadataConcurrentMap.values().forEach(callback); + } + } + + @Override + public QueueMetadata addQueue(MessageQueue mq, long baseOffset) { + QueueMetadata old = getQueue(mq); + if (old != null) { + return old; + } + QueueMetadata metadata = new QueueMetadata(mq, baseOffset, baseOffset); + queueMetadataTable.computeIfAbsent(mq.getTopic(), topic -> new ConcurrentHashMap<>()) + .put(mq.getQueueId(), metadata); + persist(); + return metadata; + } + + @Override + public void updateQueue(QueueMetadata metadata) { + MessageQueue queue = metadata.getQueue(); + if (queueMetadataTable.containsKey(queue.getTopic())) { + ConcurrentMap metadataMap = queueMetadataTable.get(queue.getTopic()); + if (metadataMap.containsKey(queue.getQueueId())) { + metadata.setUpdateTimestamp(System.currentTimeMillis()); + metadataMap.put(queue.getQueueId(), metadata); + } + persist(); + } + } + + @Override + public void deleteQueue(MessageQueue mq) { + if (queueMetadataTable.containsKey(mq.getTopic())) { + queueMetadataTable.get(mq.getTopic()).remove(mq.getQueueId()); + } + persist(); + } + + @VisibleForTesting + public Map> getTableByFileType( + FileSegmentType fileType) { + + switch (fileType) { + case COMMIT_LOG: + return commitLogFileSegmentTable; + case CONSUME_QUEUE: + return consumeQueueFileSegmentTable; + case INDEX: + return indexFileSegmentTable; + } + return new HashMap<>(); + } + + @Override + public FileSegmentMetadata getFileSegment( + String basePath, FileSegmentType fileType, long baseOffset) { + + return Optional.ofNullable(this.getTableByFileType(fileType).get(basePath)) + .map(fileMap -> fileMap.get(baseOffset)).orElse(null); + } + + @Override + public void updateFileSegment(FileSegmentMetadata fileSegmentMetadata) { + FileSegmentType fileType = + FileSegmentType.valueOf(fileSegmentMetadata.getType()); + ConcurrentMap offsetTable = this.getTableByFileType(fileType) + .computeIfAbsent(fileSegmentMetadata.getPath(), s -> new ConcurrentHashMap<>()); + offsetTable.put(fileSegmentMetadata.getBaseOffset(), fileSegmentMetadata); + persist(); + } + + @Override + public void iterateFileSegment(Consumer callback) { + commitLogFileSegmentTable + .forEach((filePath, map) -> map.forEach((offset, metadata) -> callback.accept(metadata))); + consumeQueueFileSegmentTable + .forEach((filePath, map) -> map.forEach((offset, metadata) -> callback.accept(metadata))); + indexFileSegmentTable + .forEach((filePath, map) -> map.forEach((offset, metadata) -> callback.accept(metadata))); + } + + @Override + public void iterateFileSegment(String basePath, FileSegmentType fileType, Consumer callback) { + this.getTableByFileType(fileType).getOrDefault(basePath, new ConcurrentHashMap<>()) + .forEach((offset, metadata) -> callback.accept(metadata)); + } + + @Override + public void deleteFileSegment(String filePath, FileSegmentType fileType) { + Map> offsetTable = this.getTableByFileType(fileType); + if (offsetTable != null) { + offsetTable.remove(filePath); + } + persist(); + } + + @Override + public void deleteFileSegment(String basePath, FileSegmentType fileType, long baseOffset) { + ConcurrentMap offsetTable = this.getTableByFileType(fileType).get(basePath); + if (offsetTable != null) { + offsetTable.remove(baseOffset); + } + persist(); + } + + @Override + public void destroy() { + topicSequenceNumber.set(0L); + topicMetadataTable.clear(); + queueMetadataTable.clear(); + commitLogFileSegmentTable.clear(); + consumeQueueFileSegmentTable.clear(); + indexFileSegmentTable.clear(); + persist(); + } + + static class TieredMetadataSerializeWrapper extends RemotingSerializable { + + private AtomicLong topicSerialNumber = new AtomicLong(0L); + + private ConcurrentMap topicMetadataTable; + private ConcurrentMap> queueMetadataTable; + + // Declare concurrent mapping tables to store file segment metadata + // Key: filePath -> Value: + private ConcurrentMap> commitLogFileSegmentTable; + private ConcurrentMap> consumeQueueFileSegmentTable; + private ConcurrentMap> indexFileSegmentTable; + + public TieredMetadataSerializeWrapper() { + this.topicMetadataTable = new ConcurrentHashMap<>(DEFAULT_CAPACITY); + this.queueMetadataTable = new ConcurrentHashMap<>(DEFAULT_CAPACITY); + this.commitLogFileSegmentTable = new ConcurrentHashMap<>(DEFAULT_CAPACITY); + this.consumeQueueFileSegmentTable = new ConcurrentHashMap<>(DEFAULT_CAPACITY); + this.indexFileSegmentTable = new ConcurrentHashMap<>(DEFAULT_CAPACITY); + } + + public AtomicLong getTopicSerialNumber() { + return topicSerialNumber; + } + + public void setTopicSerialNumber(AtomicLong topicSerialNumber) { + this.topicSerialNumber = topicSerialNumber; + } + + public ConcurrentMap getTopicMetadataTable() { + return topicMetadataTable; + } + + public void setTopicMetadataTable( + ConcurrentMap topicMetadataTable) { + this.topicMetadataTable = topicMetadataTable; + } + + public ConcurrentMap> getQueueMetadataTable() { + return queueMetadataTable; + } + + public void setQueueMetadataTable( + ConcurrentMap> queueMetadataTable) { + this.queueMetadataTable = queueMetadataTable; + } + + public ConcurrentMap> getCommitLogFileSegmentTable() { + return commitLogFileSegmentTable; + } + + public void setCommitLogFileSegmentTable( + ConcurrentMap> commitLogFileSegmentTable) { + this.commitLogFileSegmentTable = commitLogFileSegmentTable; + } + + public ConcurrentMap> getConsumeQueueFileSegmentTable() { + return consumeQueueFileSegmentTable; + } + + public void setConsumeQueueFileSegmentTable( + ConcurrentMap> consumeQueueFileSegmentTable) { + this.consumeQueueFileSegmentTable = consumeQueueFileSegmentTable; + } + + public ConcurrentMap> getIndexFileSegmentTable() { + return indexFileSegmentTable; + } + + public void setIndexFileSegmentTable( + ConcurrentMap> indexFileSegmentTable) { + this.indexFileSegmentTable = indexFileSegmentTable; + } + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/metadata/MetadataStore.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/metadata/MetadataStore.java new file mode 100644 index 0000000..0b05312 --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/metadata/MetadataStore.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.tieredstore.metadata; + +import java.util.function.Consumer; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.tieredstore.common.FileSegmentType; +import org.apache.rocketmq.tieredstore.metadata.entity.FileSegmentMetadata; +import org.apache.rocketmq.tieredstore.metadata.entity.QueueMetadata; +import org.apache.rocketmq.tieredstore.metadata.entity.TopicMetadata; + +/** + * Provides tiered metadata storage service to store metadata information of Topic, Queue, FileSegment, etc. + */ +public interface MetadataStore { + + /** + * Get the metadata information of specified Topic. + * + * @param topic The name of Topic. + * @return The metadata information of specified Topic, or null if it does not exist. + */ + TopicMetadata getTopic(String topic); + + /** + * Add a new metadata information of Topic. + * + * @param topic The name of Topic. + * @param reserveTime The reserve time. + * @return The newly added metadata information of Topic. + */ + TopicMetadata addTopic(String topic, long reserveTime); + + void updateTopic(TopicMetadata topicMetadata); + + void iterateTopic(Consumer callback); + + void deleteTopic(String topic); + + QueueMetadata getQueue(MessageQueue mq); + + QueueMetadata addQueue(MessageQueue mq, long baseOffset); + + void updateQueue(QueueMetadata queueMetadata); + + void iterateQueue(String topic, Consumer callback); + + void deleteQueue(MessageQueue mq); + + FileSegmentMetadata getFileSegment(String basePath, FileSegmentType fileType, long baseOffset); + + void updateFileSegment(FileSegmentMetadata fileSegmentMetadata); + + void iterateFileSegment(Consumer callback); + + void iterateFileSegment(String basePath, FileSegmentType fileType, Consumer callback); + + void deleteFileSegment(String basePath, FileSegmentType fileType); + + void deleteFileSegment(String basePath, FileSegmentType fileType, long baseOffset); + + void destroy(); +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/metadata/entity/FileSegmentMetadata.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/metadata/entity/FileSegmentMetadata.java new file mode 100644 index 0000000..4f988ca --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/metadata/entity/FileSegmentMetadata.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.tieredstore.metadata.entity; + +import com.alibaba.fastjson.annotation.JSONField; +import java.util.Objects; + +public class FileSegmentMetadata { + + public static final int STATUS_NEW = 0; + public static final int STATUS_SEALED = 1; + public static final int STATUS_DELETED = 2; + + @JSONField(ordinal = 1) + private String path; + + @JSONField(ordinal = 2) + private int type; + + @JSONField(ordinal = 3) + private long baseOffset; + + @JSONField(ordinal = 4) + private int status; + + @JSONField(ordinal = 5) + private long size; + + @JSONField(ordinal = 6) + private long createTimestamp; + + @JSONField(ordinal = 7) + private long beginTimestamp; + + @JSONField(ordinal = 8) + private long endTimestamp; + + @JSONField(ordinal = 9) + private long sealTimestamp; + + // default constructor is used by fastjson + @SuppressWarnings("unused") + public FileSegmentMetadata() { + } + + public FileSegmentMetadata(String path, long baseOffset, int type) { + this.path = path; + this.baseOffset = baseOffset; + this.type = type; + this.status = STATUS_NEW; + } + + public void markSealed() { + this.status = STATUS_SEALED; + this.sealTimestamp = System.currentTimeMillis(); + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + public long getBaseOffset() { + return baseOffset; + } + + public void setBaseOffset(long baseOffset) { + this.baseOffset = baseOffset; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public long getCreateTimestamp() { + return createTimestamp; + } + + public void setCreateTimestamp(long createTimestamp) { + this.createTimestamp = createTimestamp; + } + + public long getBeginTimestamp() { + return beginTimestamp; + } + + public void setBeginTimestamp(long beginTimestamp) { + this.beginTimestamp = beginTimestamp; + } + + public long getEndTimestamp() { + return endTimestamp; + } + + public void setEndTimestamp(long endTimestamp) { + this.endTimestamp = endTimestamp; + } + + public long getSealTimestamp() { + return sealTimestamp; + } + + public void setSealTimestamp(long sealTimestamp) { + this.sealTimestamp = sealTimestamp; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + FileSegmentMetadata metadata = (FileSegmentMetadata) o; + return size == metadata.size + && baseOffset == metadata.baseOffset + && status == metadata.status + && path.equals(metadata.path) + && type == metadata.type + && createTimestamp == metadata.createTimestamp + && beginTimestamp == metadata.beginTimestamp + && endTimestamp == metadata.endTimestamp + && sealTimestamp == metadata.sealTimestamp; + } + + @Override + public int hashCode() { + return Objects.hash(type, path, baseOffset, status, size, createTimestamp, beginTimestamp, endTimestamp, sealTimestamp); + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/metadata/entity/QueueMetadata.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/metadata/entity/QueueMetadata.java new file mode 100644 index 0000000..6720f1d --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/metadata/entity/QueueMetadata.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.tieredstore.metadata.entity; + +import com.alibaba.fastjson.annotation.JSONField; +import org.apache.rocketmq.common.message.MessageQueue; + +public class QueueMetadata { + + @JSONField(ordinal = 1) + private MessageQueue queue; + + @JSONField(ordinal = 2) + private long minOffset; + + @JSONField(ordinal = 3) + private long maxOffset; + + @JSONField(ordinal = 4) + private long updateTimestamp; + + // default constructor is used by fastjson + @SuppressWarnings("unused") + public QueueMetadata() { + } + + public QueueMetadata(MessageQueue queue, long minOffset, long maxOffset) { + this.queue = queue; + this.minOffset = minOffset; + this.maxOffset = maxOffset; + this.updateTimestamp = System.currentTimeMillis(); + } + + public MessageQueue getQueue() { + return queue; + } + + public void setQueue(MessageQueue queue) { + this.queue = queue; + } + + 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 getUpdateTimestamp() { + return updateTimestamp; + } + + public void setUpdateTimestamp(long updateTimestamp) { + this.updateTimestamp = updateTimestamp; + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/metadata/entity/TopicMetadata.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/metadata/entity/TopicMetadata.java new file mode 100644 index 0000000..80e5230 --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/metadata/entity/TopicMetadata.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.tieredstore.metadata.entity; + +import com.alibaba.fastjson.annotation.JSONField; + +public class TopicMetadata { + + @JSONField(ordinal = 1) + private long topicId; + + @JSONField(ordinal = 2) + private String topic; + + @JSONField(ordinal = 3) + private int status; + + @JSONField(ordinal = 4) + private long reserveTime; + + @JSONField(ordinal = 5) + private long updateTimestamp; + + // default constructor is used by fastjson + @SuppressWarnings("unused") + public TopicMetadata() { + } + + public TopicMetadata(long topicId, String topic, long reserveTime) { + this.topicId = topicId; + this.topic = topic; + this.reserveTime = reserveTime; + this.updateTimestamp = System.currentTimeMillis(); + } + + public long getTopicId() { + return topicId; + } + + public void setTopicId(long topicId) { + this.topicId = topicId; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public long getReserveTime() { + return reserveTime; + } + + public void setReserveTime(long reserveTime) { + this.reserveTime = reserveTime; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public long getUpdateTimestamp() { + return updateTimestamp; + } + + public void setUpdateTimestamp(long updateTimestamp) { + this.updateTimestamp = updateTimestamp; + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/metrics/TieredStoreMetricsConstant.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/metrics/TieredStoreMetricsConstant.java new file mode 100644 index 0000000..cb4674e --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/metrics/TieredStoreMetricsConstant.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.tieredstore.metrics; + +public class TieredStoreMetricsConstant { + public static final String HISTOGRAM_API_LATENCY = "rocketmq_tiered_store_api_latency"; + public static final String HISTOGRAM_PROVIDER_RPC_LATENCY = "rocketmq_tiered_store_provider_rpc_latency"; + public static final String HISTOGRAM_UPLOAD_BYTES = "rocketmq_tiered_store_provider_upload_bytes"; + public static final String HISTOGRAM_DOWNLOAD_BYTES = "rocketmq_tiered_store_provider_download_bytes"; + + public static final String GAUGE_DISPATCH_BEHIND = "rocketmq_tiered_store_dispatch_behind"; + public static final String GAUGE_DISPATCH_LATENCY = "rocketmq_tiered_store_dispatch_latency"; + public static final String COUNTER_MESSAGES_DISPATCH_TOTAL = "rocketmq_tiered_store_messages_dispatch_total"; + public static final String COUNTER_MESSAGES_OUT_TOTAL = "rocketmq_tiered_store_messages_out_total"; + public static final String COUNTER_GET_MESSAGE_FALLBACK_TOTAL = "rocketmq_tiered_store_get_message_fallback_total"; + + public static final String GAUGE_CACHE_COUNT = "rocketmq_tiered_store_read_ahead_cache_count"; + public static final String GAUGE_CACHE_BYTES = "rocketmq_tiered_store_read_ahead_cache_bytes"; + public static final String COUNTER_CACHE_ACCESS = "rocketmq_tiered_store_read_ahead_cache_access_total"; + public static final String COUNTER_CACHE_HIT = "rocketmq_tiered_store_read_ahead_cache_hit_total"; + + public static final String GAUGE_STORAGE_MESSAGE_RESERVE_TIME = "rocketmq_storage_message_reserve_time"; + + public static final String LABEL_OPERATION = "operation"; + public static final String LABEL_SUCCESS = "success"; + + public static final String LABEL_PATH = "path"; + public static final String LABEL_TOPIC = "topic"; + public static final String LABEL_GROUP = "group"; + public static final String LABEL_QUEUE_ID = "queue_id"; + public static final String LABEL_FILE_TYPE = "file_type"; + + // blob constants + public static final String STORAGE_MEDIUM_BLOB = "blob"; + + public static final String OPERATION_API_GET_MESSAGE = "get_message"; + public static final String OPERATION_API_GET_EARLIEST_MESSAGE_TIME = "get_earliest_message_time"; + public static final String OPERATION_API_GET_TIME_BY_OFFSET = "get_time_by_offset"; + public static final String OPERATION_API_GET_OFFSET_BY_TIME = "get_offset_by_time"; + public static final String OPERATION_API_QUERY_MESSAGE = "query_message"; +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/metrics/TieredStoreMetricsManager.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/metrics/TieredStoreMetricsManager.java new file mode 100644 index 0000000..4d08328 --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/metrics/TieredStoreMetricsManager.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.tieredstore.metrics; + +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 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 org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.message.MessageQueue; +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.MessageStore; +import org.apache.rocketmq.store.exception.ConsumeQueueException; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.common.FileSegmentType; +import org.apache.rocketmq.tieredstore.core.MessageStoreFetcher; +import org.apache.rocketmq.tieredstore.core.MessageStoreFetcherImpl; +import org.apache.rocketmq.tieredstore.file.FlatFileStore; +import org.apache.rocketmq.tieredstore.file.FlatMessageFile; +import org.apache.rocketmq.tieredstore.metadata.MetadataStore; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.GAUGE_STORAGE_SIZE; +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.tieredstore.metrics.TieredStoreMetricsConstant.COUNTER_CACHE_ACCESS; +import static org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsConstant.COUNTER_CACHE_HIT; +import static org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsConstant.COUNTER_GET_MESSAGE_FALLBACK_TOTAL; +import static org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsConstant.COUNTER_MESSAGES_DISPATCH_TOTAL; +import static org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsConstant.COUNTER_MESSAGES_OUT_TOTAL; +import static org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsConstant.GAUGE_CACHE_BYTES; +import static org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsConstant.GAUGE_CACHE_COUNT; +import static org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsConstant.GAUGE_DISPATCH_BEHIND; +import static org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsConstant.GAUGE_DISPATCH_LATENCY; +import static org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsConstant.GAUGE_STORAGE_MESSAGE_RESERVE_TIME; +import static org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsConstant.HISTOGRAM_API_LATENCY; +import static org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsConstant.HISTOGRAM_DOWNLOAD_BYTES; +import static org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsConstant.HISTOGRAM_PROVIDER_RPC_LATENCY; +import static org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsConstant.HISTOGRAM_UPLOAD_BYTES; +import static org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsConstant.LABEL_FILE_TYPE; +import static org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsConstant.LABEL_QUEUE_ID; +import static org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsConstant.LABEL_TOPIC; +import static org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsConstant.STORAGE_MEDIUM_BLOB; + +public class TieredStoreMetricsManager { + + private static final Logger log = LoggerFactory.getLogger(MessageStoreUtil.TIERED_STORE_LOGGER_NAME); + public static Supplier attributesBuilderSupplier; + private static String storageMedium = STORAGE_MEDIUM_BLOB; + + public static LongHistogram apiLatency = new NopLongHistogram(); + + // tiered store provider metrics + public static LongHistogram providerRpcLatency = new NopLongHistogram(); + public static LongHistogram uploadBytes = new NopLongHistogram(); + public static LongHistogram downloadBytes = new NopLongHistogram(); + + public static ObservableLongGauge dispatchBehind = new NopObservableLongGauge(); + public static ObservableLongGauge dispatchLatency = new NopObservableLongGauge(); + public static LongCounter messagesDispatchTotal = new NopLongCounter(); + public static LongCounter messagesOutTotal = new NopLongCounter(); + public static LongCounter fallbackTotal = new NopLongCounter(); + + public static ObservableLongGauge cacheCount = new NopObservableLongGauge(); + public static ObservableLongGauge cacheBytes = new NopObservableLongGauge(); + public static LongCounter cacheAccess = new NopLongCounter(); + public static LongCounter cacheHit = new NopLongCounter(); + + public static ObservableLongGauge storageSize = new NopObservableLongGauge(); + public static ObservableLongGauge storageMessageReserveTime = new NopObservableLongGauge(); + + public static List> getMetricsView() { + ArrayList> res = new ArrayList<>(); + + InstrumentSelector providerRpcLatencySelector = InstrumentSelector.builder() + .setType(InstrumentType.HISTOGRAM) + .setName(HISTOGRAM_PROVIDER_RPC_LATENCY) + .build(); + + InstrumentSelector rpcLatencySelector = InstrumentSelector.builder() + .setType(InstrumentType.HISTOGRAM) + .setName(HISTOGRAM_API_LATENCY) + .build(); + + ViewBuilder rpcLatencyViewBuilder = View.builder() + .setAggregation(Aggregation.explicitBucketHistogram(Arrays.asList(1d, 3d, 5d, 7d, 10d, 100d, 200d, 400d, 600d, 800d, 1d * 1000, 1d * 1500, 1d * 3000))) + .setDescription("tiered_store_rpc_latency_view"); + + InstrumentSelector uploadBufferSizeSelector = InstrumentSelector.builder() + .setType(InstrumentType.HISTOGRAM) + .setName(HISTOGRAM_UPLOAD_BYTES) + .build(); + + InstrumentSelector downloadBufferSizeSelector = InstrumentSelector.builder() + .setType(InstrumentType.HISTOGRAM) + .setName(HISTOGRAM_DOWNLOAD_BYTES) + .build(); + + ViewBuilder bufferSizeViewBuilder = View.builder() + .setAggregation(Aggregation.explicitBucketHistogram(Arrays.asList(1d * MessageStoreUtil.KB, 10d * MessageStoreUtil.KB, 100d * MessageStoreUtil.KB, 1d * MessageStoreUtil.MB, 10d * MessageStoreUtil.MB, 32d * MessageStoreUtil.MB, 50d * MessageStoreUtil.MB, 100d * MessageStoreUtil.MB))) + .setDescription("tiered_store_buffer_size_view"); + + res.add(new Pair<>(rpcLatencySelector, rpcLatencyViewBuilder)); + res.add(new Pair<>(providerRpcLatencySelector, rpcLatencyViewBuilder)); + res.add(new Pair<>(uploadBufferSizeSelector, bufferSizeViewBuilder)); + res.add(new Pair<>(downloadBufferSizeSelector, bufferSizeViewBuilder)); + return res; + } + + public static void setStorageMedium(String storageMedium) { + TieredStoreMetricsManager.storageMedium = storageMedium; + } + + public static void init(Meter meter, Supplier attributesBuilderSupplier, + MessageStoreConfig storeConfig, MessageStoreFetcher fetcher, + FlatFileStore flatFileStore, MessageStore next) { + + TieredStoreMetricsManager.attributesBuilderSupplier = attributesBuilderSupplier; + + apiLatency = meter.histogramBuilder(HISTOGRAM_API_LATENCY) + .setDescription("Tiered store rpc latency") + .setUnit("milliseconds") + .ofLongs() + .build(); + + providerRpcLatency = meter.histogramBuilder(HISTOGRAM_PROVIDER_RPC_LATENCY) + .setDescription("Tiered store rpc latency") + .setUnit("milliseconds") + .ofLongs() + .build(); + + uploadBytes = meter.histogramBuilder(HISTOGRAM_UPLOAD_BYTES) + .setDescription("Tiered store upload buffer size") + .setUnit("bytes") + .ofLongs() + .build(); + + downloadBytes = meter.histogramBuilder(HISTOGRAM_DOWNLOAD_BYTES) + .setDescription("Tiered store download buffer size") + .setUnit("bytes") + .ofLongs() + .build(); + + dispatchBehind = meter.gaugeBuilder(GAUGE_DISPATCH_BEHIND) + .setDescription("Tiered store dispatch behind message count") + .ofLongs() + .buildWithCallback(measurement -> { + for (FlatMessageFile flatFile : flatFileStore.deepCopyFlatFileToList()) { + try { + + MessageQueue mq = flatFile.getMessageQueue(); + long maxOffset = next.getMaxOffsetInQueue(mq.getTopic(), mq.getQueueId()); + long maxTimestamp = next.getMessageStoreTimeStamp(mq.getTopic(), mq.getQueueId(), maxOffset - 1); + if (maxTimestamp > 0 && System.currentTimeMillis() - maxTimestamp > TimeUnit.HOURS.toMillis(flatFile.getFileReservedHours())) { + continue; + } + + Attributes commitLogAttributes = newAttributesBuilder() + .put(LABEL_TOPIC, mq.getTopic()) + .put(LABEL_QUEUE_ID, mq.getQueueId()) + .put(LABEL_FILE_TYPE, FileSegmentType.COMMIT_LOG.name().toLowerCase()) + .build(); + + Attributes consumeQueueAttributes = newAttributesBuilder() + .put(LABEL_TOPIC, mq.getTopic()) + .put(LABEL_QUEUE_ID, mq.getQueueId()) + .put(LABEL_FILE_TYPE, FileSegmentType.CONSUME_QUEUE.name().toLowerCase()) + .build(); + measurement.record(Math.max(maxOffset - flatFile.getConsumeQueueMaxOffset(), 0), consumeQueueAttributes); + } catch (ConsumeQueueException e) { + // TODO: handle exception here + } + } + }); + + dispatchLatency = meter.gaugeBuilder(GAUGE_DISPATCH_LATENCY) + .setDescription("Tiered store dispatch latency") + .setUnit("seconds") + .ofLongs() + .buildWithCallback(measurement -> { + for (FlatMessageFile flatFile : flatFileStore.deepCopyFlatFileToList()) { + try { + MessageQueue mq = flatFile.getMessageQueue(); + + long maxOffset = next.getMaxOffsetInQueue(mq.getTopic(), mq.getQueueId()); + long maxTimestamp = next.getMessageStoreTimeStamp(mq.getTopic(), mq.getQueueId(), maxOffset - 1); + if (maxTimestamp > 0 && System.currentTimeMillis() - maxTimestamp > TimeUnit.HOURS.toMillis(flatFile.getFileReservedHours())) { + continue; + } + + Attributes commitLogAttributes = newAttributesBuilder() + .put(LABEL_TOPIC, mq.getTopic()) + .put(LABEL_QUEUE_ID, mq.getQueueId()) + .put(LABEL_FILE_TYPE, FileSegmentType.COMMIT_LOG.name().toLowerCase()) + .build(); + + Attributes consumeQueueAttributes = newAttributesBuilder() + .put(LABEL_TOPIC, mq.getTopic()) + .put(LABEL_QUEUE_ID, mq.getQueueId()) + .put(LABEL_FILE_TYPE, FileSegmentType.CONSUME_QUEUE.name().toLowerCase()) + .build(); + long consumeQueueDispatchOffset = flatFile.getConsumeQueueMaxOffset(); + long consumeQueueDispatchLatency = next.getMessageStoreTimeStamp(mq.getTopic(), mq.getQueueId(), consumeQueueDispatchOffset); + if (maxOffset <= consumeQueueDispatchOffset || consumeQueueDispatchLatency < 0) { + measurement.record(0, consumeQueueAttributes); + } else { + measurement.record(System.currentTimeMillis() - consumeQueueDispatchLatency, consumeQueueAttributes); + } + } catch (ConsumeQueueException e) { + // TODO: handle exception + } + } + }); + + messagesDispatchTotal = meter.counterBuilder(COUNTER_MESSAGES_DISPATCH_TOTAL) + .setDescription("Total number of dispatch messages") + .build(); + + messagesOutTotal = meter.counterBuilder(COUNTER_MESSAGES_OUT_TOTAL) + .setDescription("Total number of outgoing messages") + .build(); + + fallbackTotal = meter.counterBuilder(COUNTER_GET_MESSAGE_FALLBACK_TOTAL) + .setDescription("Total times of fallback to next store when getting message") + .build(); + + cacheCount = meter.gaugeBuilder(GAUGE_CACHE_COUNT) + .setDescription("Tiered store cache message count") + .ofLongs() + .buildWithCallback(measurement -> { + if (fetcher instanceof MessageStoreFetcherImpl) { + long count = ((MessageStoreFetcherImpl) fetcher).getFetcherCache().stats().loadCount(); + measurement.record(count, newAttributesBuilder().build()); + } + }); + + cacheBytes = meter.gaugeBuilder(GAUGE_CACHE_BYTES) + .setDescription("Tiered store cache message bytes") + .setUnit("bytes") + .ofLongs() + .buildWithCallback(measurement -> { + if (fetcher instanceof MessageStoreFetcherImpl) { + long count = ((MessageStoreFetcherImpl) fetcher).getFetcherCache().estimatedSize(); + measurement.record(count, newAttributesBuilder().build()); + } + }); + + cacheAccess = meter.counterBuilder(COUNTER_CACHE_ACCESS) + .setDescription("Tiered store cache access count") + .build(); + + cacheHit = meter.counterBuilder(COUNTER_CACHE_HIT) + .setDescription("Tiered store cache hit count") + .build(); + + storageSize = meter.gaugeBuilder(GAUGE_STORAGE_SIZE) + .setDescription("Broker storage size") + .setUnit("bytes") + .ofLongs() + .buildWithCallback(measurement -> { + Map> topicFileSizeMap = new HashMap<>(); + try { + MetadataStore metadataStore = flatFileStore.getMetadataStore(); + metadataStore.iterateFileSegment(fileSegment -> { + Map subMap = + topicFileSizeMap.computeIfAbsent(fileSegment.getPath(), k -> new HashMap<>()); + FileSegmentType fileSegmentType = + FileSegmentType.valueOf(fileSegment.getType()); + Long size = subMap.computeIfAbsent(fileSegmentType, k -> 0L); + subMap.put(fileSegmentType, size + fileSegment.getSize()); + }); + } catch (Exception e) { + log.error("Failed to get storage size", e); + } + topicFileSizeMap.forEach((topic, subMap) -> { + subMap.forEach((fileSegmentType, size) -> { + Attributes attributes = newAttributesBuilder() + .put(LABEL_TOPIC, topic) + .put(LABEL_FILE_TYPE, fileSegmentType.name().toLowerCase()) + .build(); + measurement.record(size, attributes); + }); + }); + }); + + storageMessageReserveTime = meter.gaugeBuilder(GAUGE_STORAGE_MESSAGE_RESERVE_TIME) + .setDescription("Broker message reserve time") + .setUnit("milliseconds") + .ofLongs() + .buildWithCallback(measurement -> { + for (FlatMessageFile flatFile : flatFileStore.deepCopyFlatFileToList()) { + long timestamp = flatFile.getMinStoreTimestamp(); + if (timestamp > 0) { + MessageQueue mq = flatFile.getMessageQueue(); + Attributes attributes = newAttributesBuilder() + .put(LABEL_TOPIC, mq.getTopic()) + .put(LABEL_QUEUE_ID, mq.getQueueId()) + .build(); + measurement.record(System.currentTimeMillis() - timestamp, attributes); + } + } + }); + } + + public static AttributesBuilder newAttributesBuilder() { + AttributesBuilder builder = attributesBuilderSupplier != null ? attributesBuilderSupplier.get() : Attributes.builder(); + return builder.put(LABEL_STORAGE_TYPE, "tiered") + .put(LABEL_STORAGE_MEDIUM, storageMedium); + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/provider/FileSegment.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/provider/FileSegment.java new file mode 100644 index 0000000..1140add --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/provider/FileSegment.java @@ -0,0 +1,349 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tieredstore.provider; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Semaphore; +import java.util.concurrent.locks.ReentrantLock; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.MessageStoreExecutor; +import org.apache.rocketmq.tieredstore.common.AppendResult; +import org.apache.rocketmq.tieredstore.common.FileSegmentType; +import org.apache.rocketmq.tieredstore.exception.TieredStoreErrorCode; +import org.apache.rocketmq.tieredstore.exception.TieredStoreException; +import org.apache.rocketmq.tieredstore.stream.FileSegmentInputStream; +import org.apache.rocketmq.tieredstore.stream.FileSegmentInputStreamFactory; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class FileSegment implements Comparable, FileSegmentProvider { + + private static final Logger log = LoggerFactory.getLogger(MessageStoreUtil.TIERED_STORE_LOGGER_NAME); + + protected static final Long GET_FILE_SIZE_ERROR = -1L; + + protected final long baseOffset; + protected final String filePath; + protected final FileSegmentType fileType; + protected final MessageStoreConfig storeConfig; + + protected final long maxSize; + protected final MessageStoreExecutor executor; + protected final ReentrantLock fileLock = new ReentrantLock(); + protected final Semaphore commitLock = new Semaphore(1); + + protected volatile boolean closed = false; + protected volatile long minTimestamp = Long.MAX_VALUE; + protected volatile long maxTimestamp = Long.MAX_VALUE; + protected volatile long commitPosition = 0L; + protected volatile long appendPosition = 0L; + + protected volatile List bufferList = new ArrayList<>(); + protected volatile FileSegmentInputStream fileSegmentInputStream; + protected volatile CompletableFuture flightCommitRequest; + + public FileSegment(MessageStoreConfig storeConfig, FileSegmentType fileType, + String filePath, long baseOffset, MessageStoreExecutor executor) { + + this.storeConfig = storeConfig; + this.fileType = fileType; + this.filePath = filePath; + this.baseOffset = baseOffset; + this.executor = executor; + this.maxSize = this.getMaxSizeByFileType(); + } + + @Override + public int compareTo(FileSegment o) { + return Long.compare(this.baseOffset, o.baseOffset); + } + + public long getBaseOffset() { + return baseOffset; + } + + public void initPosition(long pos) { + fileLock.lock(); + try { + this.commitPosition = pos; + this.appendPosition = pos; + } finally { + fileLock.unlock(); + } + } + + public long getCommitPosition() { + return commitPosition; + } + + public long getAppendPosition() { + return appendPosition; + } + + public long getCommitOffset() { + return baseOffset + commitPosition; + } + + public long getAppendOffset() { + return baseOffset + appendPosition; + } + + public FileSegmentType getFileType() { + return fileType; + } + + public long getMaxSizeByFileType() { + switch (fileType) { + case COMMIT_LOG: + return storeConfig.getTieredStoreCommitLogMaxSize(); + case CONSUME_QUEUE: + return storeConfig.getTieredStoreConsumeQueueMaxSize(); + case INDEX: + default: + return Long.MAX_VALUE; + } + } + + public long getMaxSize() { + return maxSize; + } + + public long getMinTimestamp() { + return minTimestamp; + } + + public void setMinTimestamp(long minTimestamp) { + this.minTimestamp = minTimestamp; + } + + public long getMaxTimestamp() { + return maxTimestamp; + } + + public void setMaxTimestamp(long maxTimestamp) { + this.maxTimestamp = maxTimestamp; + } + + public boolean isClosed() { + return closed; + } + + public void close() { + fileLock.lock(); + try { + this.closed = true; + } finally { + fileLock.unlock(); + } + } + + protected List borrowBuffer() { + List temp; + fileLock.lock(); + try { + temp = bufferList; + bufferList = new ArrayList<>(); + } finally { + fileLock.unlock(); + } + return temp; + } + + @SuppressWarnings("NonAtomicOperationOnVolatileField") + protected void updateTimestamp(long timestamp) { + fileLock.lock(); + try { + if (maxTimestamp == Long.MAX_VALUE && minTimestamp == Long.MAX_VALUE) { + maxTimestamp = timestamp; + minTimestamp = timestamp; + return; + } + maxTimestamp = Math.max(maxTimestamp, timestamp); + minTimestamp = Math.min(minTimestamp, timestamp); + } finally { + fileLock.unlock(); + } + } + + @SuppressWarnings("NonAtomicOperationOnVolatileField") + public AppendResult append(ByteBuffer buffer, long timestamp) { + fileLock.lock(); + try { + if (closed) { + return AppendResult.FILE_CLOSED; + } + if (appendPosition + buffer.remaining() > maxSize) { + return AppendResult.FILE_FULL; + } + if (bufferList.size() >= storeConfig.getTieredStoreMaxGroupCommitCount()) { + return AppendResult.BUFFER_FULL; + } + this.appendPosition += buffer.remaining(); + this.bufferList.add(buffer); + this.updateTimestamp(timestamp); + } finally { + fileLock.unlock(); + } + return AppendResult.SUCCESS; + } + + public boolean needCommit() { + return appendPosition > commitPosition; + } + + @SuppressWarnings("NonAtomicOperationOnVolatileField") + public CompletableFuture commitAsync() { + if (closed) { + return CompletableFuture.completedFuture(false); + } + + if (!needCommit()) { + return CompletableFuture.completedFuture(true); + } + + // acquire lock + if (commitLock.drainPermits() <= 0) { + return CompletableFuture.completedFuture(false); + } + + // handle last commit error + if (fileSegmentInputStream != null) { + long fileSize = this.getSize(); + if (fileSize == GET_FILE_SIZE_ERROR) { + log.error("FileSegment correct position error, fileName={}, commit={}, append={}, buffer={}", + this.getPath(), commitPosition, appendPosition, fileSegmentInputStream.getContentLength()); + releaseCommitLock(); + return CompletableFuture.completedFuture(false); + } + if (correctPosition(fileSize)) { + fileSegmentInputStream = null; + } + } + + int bufferSize; + if (fileSegmentInputStream != null) { + fileSegmentInputStream.rewind(); + bufferSize = fileSegmentInputStream.available(); + } else { + List bufferList = this.borrowBuffer(); + bufferSize = bufferList.stream().mapToInt(ByteBuffer::remaining).sum(); + if (bufferSize == 0) { + releaseCommitLock(); + return CompletableFuture.completedFuture(true); + } + fileSegmentInputStream = FileSegmentInputStreamFactory.build( + fileType, this.getCommitOffset(), bufferList, null, bufferSize); + } + + boolean append = fileType != FileSegmentType.INDEX; + return flightCommitRequest = + this.commit0(fileSegmentInputStream, commitPosition, bufferSize, append) + .thenApply(result -> { + if (result) { + commitPosition += bufferSize; + fileSegmentInputStream = null; + return true; + } else { + fileSegmentInputStream.rewind(); + return false; + } + }) + .exceptionally(this::handleCommitException) + .whenComplete((result, e) -> releaseCommitLock()); + } + + private boolean handleCommitException(Throwable e) { + + log.warn("FileSegment commit exception, filePath={}", this.filePath, e); + + // Get root cause here + Throwable rootCause = e.getCause() != null ? e.getCause() : e; + + long fileSize = rootCause instanceof TieredStoreException ? + ((TieredStoreException) rootCause).getPosition() : this.getSize(); + + long expectPosition = commitPosition + fileSegmentInputStream.getContentLength(); + if (fileSize == GET_FILE_SIZE_ERROR) { + log.error("Get file size error after commit, FileName: {}, Commit: {}, Content: {}, Expect: {}, Append: {}", + this.getPath(), commitPosition, fileSegmentInputStream.getContentLength(), expectPosition, appendPosition); + return false; + } + + if (correctPosition(fileSize)) { + fileSegmentInputStream = null; + return true; + } else { + fileSegmentInputStream.rewind(); + return false; + } + } + + private void releaseCommitLock() { + if (commitLock.availablePermits() == 0) { + commitLock.release(); + } + } + + /** + * return true to clear buffer + */ + private boolean correctPosition(long fileSize) { + + // Current we have three offsets here: commit offset, expect offset, file size. + // We guarantee that the commit offset is less than or equal to the expect offset. + // Max offset will increase because we can continuously put in new buffers + + // We are believing that the file size returned by the server is correct, + // can reset the commit offset to the file size reported by the storage system. + + long expectPosition = commitPosition + fileSegmentInputStream.getContentLength(); + commitPosition = fileSize; + return expectPosition == fileSize; + } + + public ByteBuffer read(long position, int length) { + return readAsync(position, length).join(); + } + + public CompletableFuture readAsync(long position, int length) { + CompletableFuture future = new CompletableFuture<>(); + if (position < 0 || position >= commitPosition) { + future.completeExceptionally(new TieredStoreException( + TieredStoreErrorCode.ILLEGAL_PARAM, "FileSegment read position is illegal position")); + return future; + } + + if (length <= 0) { + future.completeExceptionally(new TieredStoreException( + TieredStoreErrorCode.ILLEGAL_PARAM, "FileSegment read length illegal")); + return future; + } + + int readableBytes = (int) (commitPosition - position); + if (readableBytes < length) { + length = readableBytes; + log.debug("FileSegment#readAsync, expect request position is greater than commit position, " + + "file: {}, request position: {}, commit position: {}, change length from {} to {}", + getPath(), position, commitPosition, length, readableBytes); + } + return this.read0(position, length); + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/provider/FileSegmentFactory.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/provider/FileSegmentFactory.java new file mode 100644 index 0000000..ace6d8f --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/provider/FileSegmentFactory.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.tieredstore.provider; + +import java.lang.reflect.Constructor; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.MessageStoreExecutor; +import org.apache.rocketmq.tieredstore.common.FileSegmentType; +import org.apache.rocketmq.tieredstore.metadata.MetadataStore; + +public class FileSegmentFactory { + + private final MetadataStore metadataStore; + private final MessageStoreConfig storeConfig; + private final MessageStoreExecutor executor; + private final Constructor fileSegmentConstructor; + + public FileSegmentFactory(MetadataStore metadataStore, + MessageStoreConfig storeConfig, MessageStoreExecutor executor) { + + try { + this.storeConfig = storeConfig; + this.metadataStore = metadataStore; + this.executor = executor; + Class clazz = + Class.forName(storeConfig.getTieredBackendServiceProvider()).asSubclass(FileSegment.class); + fileSegmentConstructor = clazz.getConstructor( + MessageStoreConfig.class, FileSegmentType.class, String.class, Long.TYPE, MessageStoreExecutor.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public MetadataStore getMetadataStore() { + return metadataStore; + } + + public MessageStoreConfig getStoreConfig() { + return storeConfig; + } + + public FileSegment createSegment(FileSegmentType fileType, String filePath, long baseOffset) { + try { + return fileSegmentConstructor.newInstance(this.storeConfig, fileType, filePath, baseOffset, executor); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public FileSegment createCommitLogFileSegment(String filePath, long baseOffset) { + return this.createSegment(FileSegmentType.COMMIT_LOG, filePath, baseOffset); + } + + public FileSegment createConsumeQueueFileSegment(String filePath, long baseOffset) { + return this.createSegment(FileSegmentType.CONSUME_QUEUE, filePath, baseOffset); + } + + public FileSegment createIndexServiceFileSegment(String filePath, long baseOffset) { + return this.createSegment(FileSegmentType.INDEX, filePath, baseOffset); + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/provider/FileSegmentProvider.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/provider/FileSegmentProvider.java new file mode 100644 index 0000000..1ce643e --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/provider/FileSegmentProvider.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.tieredstore.provider; + +import java.nio.ByteBuffer; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.tieredstore.stream.FileSegmentInputStream; + +public interface FileSegmentProvider { + + /** + * Get file path in backend file system + * + * @return file real path + */ + String getPath(); + + /** + * Get the real length of the file. + * Return 0 if the file does not exist, + * Return -1 if system get size failed. + * + * @return file real size + */ + long getSize(); + + /** + * Is file exists in backend file system + * + * @return true if file with given path exists; false otherwise + */ + boolean exists(); + + /** + * Create file in backend file system + */ + void createFile(); + + /** + * Destroy file with given path in backend file system + */ + void destroyFile(); + + /** + * Get data from backend file system + * + * @param position the index from where the file will be read + * @param length the data size will be read + * @return data to be read + */ + CompletableFuture read0(long position, int length); + + /** + * Put data to backend file system + * + * @param inputStream data stream + * @param position backend file position to put, used in append mode + * @param length data size in stream + * @param append try to append or create a new file + * @return put result, true if data successfully write; false otherwise + */ + CompletableFuture commit0(FileSegmentInputStream inputStream, long position, int length, boolean append); +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/provider/MemoryFileSegment.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/provider/MemoryFileSegment.java new file mode 100644 index 0000000..93ad745 --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/provider/MemoryFileSegment.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.tieredstore.provider; + +import java.nio.ByteBuffer; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.MessageStoreExecutor; +import org.apache.rocketmq.tieredstore.common.FileSegmentType; +import org.apache.rocketmq.tieredstore.stream.FileSegmentInputStream; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MemoryFileSegment extends FileSegment { + + private static final Logger log = LoggerFactory.getLogger(MessageStoreUtil.TIERED_STORE_LOGGER_NAME); + + protected final ByteBuffer memStore; + protected CompletableFuture blocker; + protected int size = 0; + protected boolean checkSize = true; + + public MemoryFileSegment(MessageStoreConfig storeConfig, + FileSegmentType fileType, String filePath, long baseOffset, MessageStoreExecutor executor) { + + super(storeConfig, fileType, filePath, baseOffset, executor); + memStore = ByteBuffer.allocate(10000); + memStore.position((int) getSize()); + } + + @Override + public boolean exists() { + return false; + } + + @Override + public void createFile() { + } + + public ByteBuffer getMemStore() { + return memStore; + } + + public void setCheckSize(boolean checkSize) { + this.checkSize = checkSize; + } + + @Override + public String getPath() { + return filePath; + } + + @Override + public long getSize() { + if (checkSize) { + return 1000; + } + return size; + } + + public void setSize(int size) { + this.size = size; + } + + @Override + public CompletableFuture read0(long position, int length) { + ByteBuffer buffer = memStore.duplicate(); + buffer.position((int) position); + ByteBuffer slice = buffer.slice(); + slice.limit(length); + return CompletableFuture.completedFuture(slice); + } + + @Override + public CompletableFuture commit0( + FileSegmentInputStream inputStream, long position, int length, boolean append) { + + try { + if (blocker != null && !blocker.get()) { + log.info("Commit Blocker Exception for Memory Test"); + return CompletableFuture.completedFuture(false); + } + + int len; + byte[] buffer = new byte[1024]; + while ((len = inputStream.read(buffer)) > 0) { + memStore.put(buffer, 0, len); + } + } catch (Exception e) { + return CompletableFuture.completedFuture(false); + } + return CompletableFuture.completedFuture(true); + } + + @Override + public void destroyFile() { + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/provider/PosixFileSegment.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/provider/PosixFileSegment.java new file mode 100644 index 0000000..3ab5914 --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/provider/PosixFileSegment.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.tieredstore.provider; + +import com.google.common.base.Stopwatch; +import com.google.common.base.Supplier; +import com.google.common.io.ByteStreams; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Paths; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.MessageStoreExecutor; +import org.apache.rocketmq.tieredstore.common.FileSegmentType; +import org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsManager; +import org.apache.rocketmq.tieredstore.stream.FileSegmentInputStream; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsConstant.LABEL_FILE_TYPE; +import static org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsConstant.LABEL_OPERATION; +import static org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsConstant.LABEL_PATH; +import static org.apache.rocketmq.tieredstore.metrics.TieredStoreMetricsConstant.LABEL_SUCCESS; + +/** + * this class is experimental and may change without notice. + */ +public class PosixFileSegment extends FileSegment { + + private static final Logger log = LoggerFactory.getLogger(MessageStoreUtil.TIERED_STORE_LOGGER_NAME); + + private static final String OPERATION_POSIX_READ = "read"; + private static final String OPERATION_POSIX_WRITE = "write"; + + private final String fullPath; + private volatile File file; + private volatile FileChannel readFileChannel; + private volatile FileChannel writeFileChannel; + + public PosixFileSegment(MessageStoreConfig storeConfig, + FileSegmentType fileType, String filePath, long baseOffset, MessageStoreExecutor executor) { + + super(storeConfig, fileType, filePath, baseOffset, executor); + + // basePath + String basePath = StringUtils.defaultString(storeConfig.getTieredStoreFilePath(), + StringUtils.appendIfMissing(storeConfig.getTieredStoreFilePath(), File.separator)); + + // fullPath: basePath/hash_cluster/broker/topic/queueId/fileType/baseOffset + String clusterName = storeConfig.getBrokerClusterName(); + String clusterBasePath = String.format("%s_%s", MessageStoreUtil.getHash(clusterName), clusterName); + fullPath = Paths.get(basePath, clusterBasePath, filePath, + fileType.toString(), MessageStoreUtil.offset2FileName(baseOffset)).toString(); + log.info("Constructing Posix FileSegment, filePath: {}", fullPath); + + this.createFile(); + } + + protected AttributesBuilder newAttributesBuilder() { + return TieredStoreMetricsManager.newAttributesBuilder() + .put(LABEL_PATH, filePath) + .put(LABEL_FILE_TYPE, fileType.name().toLowerCase()); + } + + @Override + public String getPath() { + return filePath; + } + + @Override + public long getSize() { + if (exists()) { + return file.length(); + } + return 0L; + } + + @Override + public boolean exists() { + return file != null && file.exists(); + } + + @Override + public void createFile() { + if (this.file == null) { + synchronized (this) { + if (this.file == null) { + this.createFile0(); + } + } + } + } + + @SuppressWarnings({"resource", "ResultOfMethodCallIgnored"}) + private void createFile0() { + try { + File file = new File(fullPath); + File dir = file.getParentFile(); + if (!dir.exists()) { + dir.mkdirs(); + } + if (!file.exists()) { + if (file.createNewFile()) { + log.debug("Create Posix FileSegment, filePath: {}", fullPath); + } + } + this.readFileChannel = new RandomAccessFile(file, "r").getChannel(); + this.writeFileChannel = new RandomAccessFile(file, "rwd").getChannel(); + this.file = file; + } catch (Exception e) { + log.error("PosixFileSegment#createFile: create file {} failed: ", filePath, e); + } + } + + @Override + + public void destroyFile() { + this.close(); + if (file != null && file.exists()) { + if (file.delete()) { + log.info("Destroy Posix FileSegment, filePath: {}", fullPath); + } else { + log.warn("Destroy Posix FileSegment error, filePath: {}", fullPath); + } + } + } + + @Override + public void close() { + super.close(); + try { + if (readFileChannel != null && readFileChannel.isOpen()) { + readFileChannel.close(); + readFileChannel = null; + } + if (writeFileChannel != null && writeFileChannel.isOpen()) { + writeFileChannel.close(); + writeFileChannel = null; + } + } catch (IOException e) { + log.error("Destroy Posix FileSegment failed, filePath: {}", fullPath, e); + } + } + + @Override + public CompletableFuture read0(long position, int length) { + Stopwatch stopwatch = Stopwatch.createStarted(); + AttributesBuilder attributesBuilder = newAttributesBuilder() + .put(LABEL_OPERATION, OPERATION_POSIX_READ); + + return CompletableFuture.supplyAsync((Supplier) () -> { + ByteBuffer byteBuffer = ByteBuffer.allocate(length); + try { + readFileChannel.position(position); + readFileChannel.read(byteBuffer); + byteBuffer.flip(); + byteBuffer.limit(length); + + attributesBuilder.put(LABEL_SUCCESS, true); + long costTime = stopwatch.stop().elapsed(TimeUnit.MILLISECONDS); + TieredStoreMetricsManager.providerRpcLatency.record(costTime, attributesBuilder.build()); + + Attributes metricsAttributes = newAttributesBuilder() + .put(LABEL_OPERATION, OPERATION_POSIX_READ) + .build(); + int downloadedBytes = byteBuffer.remaining(); + TieredStoreMetricsManager.downloadBytes.record(downloadedBytes, metricsAttributes); + } catch (IOException e) { + long costTime = stopwatch.stop().elapsed(TimeUnit.MILLISECONDS); + attributesBuilder.put(LABEL_SUCCESS, false); + TieredStoreMetricsManager.providerRpcLatency.record(costTime, attributesBuilder.build()); + } + return byteBuffer; + }, executor.bufferFetchExecutor); + } + + @Override + @SuppressWarnings("ResultOfMethodCallIgnored") + public CompletableFuture commit0( + FileSegmentInputStream inputStream, long position, int length, boolean append) { + + Stopwatch stopwatch = Stopwatch.createStarted(); + AttributesBuilder attributesBuilder = newAttributesBuilder() + .put(LABEL_OPERATION, OPERATION_POSIX_WRITE); + + return CompletableFuture.supplyAsync(() -> { + try { + byte[] byteArray = ByteStreams.toByteArray(inputStream); + writeFileChannel.position(position); + ByteBuffer buffer = ByteBuffer.wrap(byteArray); + while (buffer.hasRemaining()) { + writeFileChannel.write(buffer); + } + writeFileChannel.force(true); + attributesBuilder.put(LABEL_SUCCESS, true); + long costTime = stopwatch.stop().elapsed(TimeUnit.MILLISECONDS); + TieredStoreMetricsManager.providerRpcLatency.record(costTime, attributesBuilder.build()); + + Attributes metricsAttributes = newAttributesBuilder() + .put(LABEL_OPERATION, OPERATION_POSIX_WRITE) + .build(); + TieredStoreMetricsManager.uploadBytes.record(length, metricsAttributes); + } catch (Exception e) { + long costTime = stopwatch.stop().elapsed(TimeUnit.MILLISECONDS); + attributesBuilder.put(LABEL_SUCCESS, false); + TieredStoreMetricsManager.providerRpcLatency.record(costTime, attributesBuilder.build()); + return false; + } + return true; + }, executor.bufferCommitExecutor); + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/stream/CommitLogInputStream.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/stream/CommitLogInputStream.java new file mode 100644 index 0000000..e2d7354 --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/stream/CommitLogInputStream.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.tieredstore.stream; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import org.apache.rocketmq.tieredstore.common.FileSegmentType; +import org.apache.rocketmq.tieredstore.util.MessageFormatUtil; + +public class CommitLogInputStream extends FileSegmentInputStream { + + /** + * commitLogOffset is the real physical offset of the commitLog buffer which is being read + */ + private final long startCommitLogOffset; + + private long commitLogOffset; + + private final ByteBuffer codaBuffer; + + private long markCommitLogOffset = -1; + + public CommitLogInputStream(FileSegmentType fileType, long startOffset, + List uploadBufferList, ByteBuffer codaBuffer, int contentLength) { + super(fileType, uploadBufferList, contentLength); + this.startCommitLogOffset = startOffset; + this.commitLogOffset = startOffset; + this.codaBuffer = codaBuffer; + } + + @Override + public synchronized void mark(int ignore) { + super.mark(ignore); + this.markCommitLogOffset = commitLogOffset; + } + + @Override + public synchronized void reset() throws IOException { + super.reset(); + this.commitLogOffset = markCommitLogOffset; + } + + @Override + public synchronized void rewind() { + super.rewind(); + this.commitLogOffset = this.startCommitLogOffset; + if (this.codaBuffer != null) { + this.codaBuffer.rewind(); + } + } + + @Override + public ByteBuffer getCodaBuffer() { + return this.codaBuffer; + } + + @Override + public int read() { + if (available() <= 0) { + return -1; + } + readPosition++; + if (curReadBufferIndex >= bufferList.size()) { + return readCoda(); + } + int res; + if (readPosInCurBuffer >= curBuffer.remaining()) { + curReadBufferIndex++; + if (curReadBufferIndex >= bufferList.size()) { + readPosInCurBuffer = 0; + return readCoda(); + } + curBuffer = bufferList.get(curReadBufferIndex); + commitLogOffset += readPosInCurBuffer; + readPosInCurBuffer = 0; + } + if (readPosInCurBuffer >= MessageFormatUtil.PHYSICAL_OFFSET_POSITION + && readPosInCurBuffer < MessageFormatUtil.SYS_FLAG_OFFSET_POSITION) { + res = (int) ((commitLogOffset >> (8 * (MessageFormatUtil.SYS_FLAG_OFFSET_POSITION - readPosInCurBuffer - 1))) & 0xff); + readPosInCurBuffer++; + } else { + res = curBuffer.get(readPosInCurBuffer++) & 0xff; + } + return res; + } + + private int readCoda() { + if (codaBuffer == null || readPosInCurBuffer >= codaBuffer.remaining()) { + return -1; + } + return codaBuffer.get(readPosInCurBuffer++) & 0xff; + } + + @Override + public int read(byte[] b, int off, int len) { + if (b == null) { + throw new NullPointerException(); + } else if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException("off < 0 || len < 0 || len > b.length - off"); + } + if (readPosition >= contentLength) { + return -1; + } + + int available = available(); + if (len > available) { + len = available; + } + if (len <= 0) { + return 0; + } + int needRead = len; + int pos = readPosition; + int bufIndex = curReadBufferIndex; + int posInCurBuffer = readPosInCurBuffer; + long curCommitLogOffset = commitLogOffset; + ByteBuffer curBuf = curBuffer; + while (needRead > 0 && bufIndex <= bufferList.size()) { + int readLen, remaining, realReadLen = 0; + if (bufIndex == bufferList.size()) { + // read from coda buffer + remaining = codaBuffer.remaining() - posInCurBuffer; + readLen = Math.min(remaining, needRead); + codaBuffer.position(posInCurBuffer); + codaBuffer.get(b, off, readLen); + codaBuffer.position(0); + // update flags + off += readLen; + needRead -= readLen; + pos += readLen; + posInCurBuffer += readLen; + continue; + } + remaining = curBuf.remaining() - posInCurBuffer; + readLen = Math.min(remaining, needRead); + curBuf = bufferList.get(bufIndex); + if (posInCurBuffer < MessageFormatUtil.PHYSICAL_OFFSET_POSITION) { + realReadLen = Math.min(MessageFormatUtil.PHYSICAL_OFFSET_POSITION - posInCurBuffer, readLen); + // read from commitLog buffer + curBuf.position(posInCurBuffer); + curBuf.get(b, off, realReadLen); + curBuf.position(0); + } else if (posInCurBuffer < MessageFormatUtil.SYS_FLAG_OFFSET_POSITION) { + realReadLen = Math.min(MessageFormatUtil.SYS_FLAG_OFFSET_POSITION - posInCurBuffer, readLen); + // read from converted PHYSICAL_OFFSET_POSITION + byte[] physicalOffsetBytes = new byte[realReadLen]; + for (int i = 0; i < realReadLen; i++) { + physicalOffsetBytes[i] = (byte) ((curCommitLogOffset >> (8 * (MessageFormatUtil.SYS_FLAG_OFFSET_POSITION - posInCurBuffer - i - 1))) & 0xff); + } + System.arraycopy(physicalOffsetBytes, 0, b, off, realReadLen); + } else { + realReadLen = readLen; + // read from commitLog buffer + curBuf.position(posInCurBuffer); + curBuf.get(b, off, readLen); + curBuf.position(0); + } + // update flags + off += realReadLen; + needRead -= realReadLen; + pos += realReadLen; + posInCurBuffer += realReadLen; + if (posInCurBuffer == curBuf.remaining()) { + // read from next buf + bufIndex++; + curCommitLogOffset += posInCurBuffer; + posInCurBuffer = 0; + } + } + readPosition = pos; + curReadBufferIndex = bufIndex; + readPosInCurBuffer = posInCurBuffer; + commitLogOffset = curCommitLogOffset; + curBuffer = curBuf; + return len; + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/stream/FileSegmentInputStream.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/stream/FileSegmentInputStream.java new file mode 100644 index 0000000..5020ff1 --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/stream/FileSegmentInputStream.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.tieredstore.stream; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.List; +import org.apache.commons.collections.CollectionUtils; +import org.apache.rocketmq.tieredstore.common.FileSegmentType; + +public class FileSegmentInputStream extends InputStream { + + /** + * file type, can be commitlog, consume queue or indexfile now + */ + protected final FileSegmentType fileType; + + /** + * hold bytebuffer + */ + protected final List bufferList; + + /** + * total remaining of bytebuffer list + */ + protected final int contentLength; + + /** + * readPosition is the now position in the stream + */ + protected int readPosition = 0; + + /** + * curReadBufferIndex is the index of the buffer in uploadBufferList which is being read + */ + protected int curReadBufferIndex = 0; + /** + * readPosInCurBuffer is the position in the buffer which is being read + */ + protected int readPosInCurBuffer = 0; + + /** + * curBuffer is the buffer which is being read, it is the same as uploadBufferList.get(curReadBufferIndex) + */ + protected ByteBuffer curBuffer; + + private int markReadPosition = -1; + + private int markCurReadBufferIndex = -1; + + private int markReadPosInCurBuffer = -1; + + public FileSegmentInputStream( + FileSegmentType fileType, List bufferList, int contentLength) { + this.fileType = fileType; + this.contentLength = contentLength; + this.bufferList = bufferList; + if (bufferList != null && bufferList.size() > 0) { + this.curBuffer = bufferList.get(curReadBufferIndex); + } + } + + @Override + public boolean markSupported() { + return true; + } + + @Override + public synchronized void mark(int ignore) { + this.markReadPosition = readPosition; + this.markCurReadBufferIndex = curReadBufferIndex; + this.markReadPosInCurBuffer = readPosInCurBuffer; + } + + @Override + public synchronized void reset() throws IOException { + if (this.markReadPosition == -1) { + throw new IOException("mark not set"); + } + this.readPosition = markReadPosition; + this.curReadBufferIndex = markCurReadBufferIndex; + this.readPosInCurBuffer = markReadPosInCurBuffer; + if (this.curReadBufferIndex < bufferList.size()) { + this.curBuffer = bufferList.get(curReadBufferIndex); + } + } + + public synchronized void rewind() { + this.readPosition = 0; + this.curReadBufferIndex = 0; + this.readPosInCurBuffer = 0; + if (CollectionUtils.isNotEmpty(bufferList)) { + this.curBuffer = bufferList.get(0); + for (ByteBuffer buffer : bufferList) { + buffer.rewind(); + } + } + } + + public int getContentLength() { + return contentLength; + } + + @Override + public int available() { + return contentLength - readPosition; + } + + public List getBufferList() { + return bufferList; + } + + public ByteBuffer getCodaBuffer() { + return null; + } + + @Override + public int read() { + if (available() <= 0) { + return -1; + } + readPosition++; + if (readPosInCurBuffer >= curBuffer.remaining()) { + curReadBufferIndex++; + if (curReadBufferIndex >= bufferList.size()) { + return -1; + } + curBuffer = bufferList.get(curReadBufferIndex); + readPosInCurBuffer = 0; + } + return curBuffer.get(readPosInCurBuffer++) & 0xff; + } + + @Override + public int read(byte[] b, int off, int len) { + if (b == null) { + throw new NullPointerException(); + } else if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException("off < 0 || len < 0 || len > b.length - off"); + } + if (readPosition >= contentLength) { + return -1; + } + + int available = available(); + if (len > available) { + len = available; + } + if (len <= 0) { + return 0; + } + int needRead = len; + int pos = readPosition; + int bufIndex = curReadBufferIndex; + int posInCurBuffer = readPosInCurBuffer; + ByteBuffer curBuf = curBuffer; + while (needRead > 0 && bufIndex < bufferList.size()) { + curBuf = bufferList.get(bufIndex); + int remaining = curBuf.remaining() - posInCurBuffer; + int readLen = Math.min(remaining, needRead); + // read from curBuf + curBuf.position(posInCurBuffer); + curBuf.get(b, off, readLen); + curBuf.position(0); + // update flags + off += readLen; + needRead -= readLen; + pos += readLen; + posInCurBuffer += readLen; + if (posInCurBuffer == curBuf.remaining()) { + // read from next buf + bufIndex++; + posInCurBuffer = 0; + } + } + readPosition = pos; + curReadBufferIndex = bufIndex; + readPosInCurBuffer = posInCurBuffer; + curBuffer = curBuf; + return len; + } +} + diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/stream/FileSegmentInputStreamFactory.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/stream/FileSegmentInputStreamFactory.java new file mode 100644 index 0000000..6872296 --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/stream/FileSegmentInputStreamFactory.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.tieredstore.stream; + +import java.nio.ByteBuffer; +import java.util.List; +import org.apache.rocketmq.tieredstore.common.FileSegmentType; + +public class FileSegmentInputStreamFactory { + + public static FileSegmentInputStream build( + FileSegmentType fileType, long offset, List bufferList, ByteBuffer byteBuffer, int length) { + + if (bufferList == null) { + throw new IllegalArgumentException("bufferList is null"); + } + + switch (fileType) { + case COMMIT_LOG: + return new CommitLogInputStream(fileType, offset, bufferList, byteBuffer, length); + case CONSUME_QUEUE: + return new FileSegmentInputStream(fileType, bufferList, length); + case INDEX: + if (bufferList.size() != 1) { + throw new IllegalArgumentException("buffer block size must be 1 when file type is IndexFile"); + } + return new FileSegmentInputStream(fileType, bufferList, length); + default: + throw new IllegalArgumentException("file type not supported"); + } + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/util/MessageFormatUtil.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/util/MessageFormatUtil.java new file mode 100644 index 0000000..560234d --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/util/MessageFormatUtil.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.tieredstore.util; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.tieredstore.common.SelectBufferResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MessageFormatUtil { + + private static final Logger log = LoggerFactory.getLogger(MessageStoreUtil.TIERED_STORE_LOGGER_NAME); + + public static final int MSG_ID_LENGTH = 8 + 8; + public static final int MAGIC_CODE_POSITION = 4; + public static final int QUEUE_OFFSET_POSITION = 20; + public static final int PHYSICAL_OFFSET_POSITION = 28; + public static final int SYS_FLAG_OFFSET_POSITION = 36; + public static final int STORE_TIMESTAMP_POSITION = 56; + public static final int STORE_HOST_POSITION = 64; + + /** + * item size: int, 4 bytes + * magic code: int, 4 bytes + * max store timestamp: long, 8 bytes + */ + public static final int COMMIT_LOG_CODA_SIZE = 4 + 8 + 4; + public static final int BLANK_MAGIC_CODE = 0xBBCCDDEE ^ 1880681586 + 8; + + /** + * commit log offset: long, 8 bytes + * message size: int, 4 bytes + * tag hash code: long, 8 bytes + */ + public static final int CONSUME_QUEUE_UNIT_SIZE = 8 + 4 + 8; + + public static int getTotalSize(ByteBuffer message) { + return message.getInt(message.position()); + } + + public static int getMagicCode(ByteBuffer message) { + return message.getInt(message.position() + MAGIC_CODE_POSITION); + } + + public static long getQueueOffset(ByteBuffer message) { + return message.getLong(message.position() + QUEUE_OFFSET_POSITION); + } + + public static long getCommitLogOffset(ByteBuffer message) { + return message.getLong(message.position() + PHYSICAL_OFFSET_POSITION); + } + + public static long getStoreTimeStamp(ByteBuffer message) { + return message.getLong(message.position() + STORE_TIMESTAMP_POSITION); + } + + public static ByteBuffer getOffsetIdBuffer(ByteBuffer message) { + ByteBuffer buffer = ByteBuffer.allocate(MSG_ID_LENGTH); + buffer.putLong(message.getLong(message.position() + STORE_HOST_POSITION)); + buffer.putLong(getCommitLogOffset(message)); + buffer.flip(); + return buffer; + } + + public static String getOffsetId(ByteBuffer message) { + return UtilAll.bytes2string(getOffsetIdBuffer(message).array()); + } + + public static Map getProperties(ByteBuffer message) { + return MessageDecoder.decodeProperties(message.slice()); + } + + public static long getCommitLogOffsetFromItem(ByteBuffer cqItem) { + return cqItem.getLong(cqItem.position()); + } + + public static int getSizeFromItem(ByteBuffer cqItem) { + return cqItem.getInt(cqItem.position() + 8); + } + + public static long getTagCodeFromItem(ByteBuffer cqItem) { + return cqItem.getLong(cqItem.position() + 12); + } + + public static List splitMessageBuffer(ByteBuffer cqBuffer, ByteBuffer msgBuffer) { + + if (cqBuffer == null || msgBuffer == null) { + log.error("MessageFormatUtil split buffer error, cq buffer or msg buffer is null"); + return new ArrayList<>(); + } + + cqBuffer.rewind(); + msgBuffer.rewind(); + + List bufferResultList = new ArrayList<>( + cqBuffer.remaining() / CONSUME_QUEUE_UNIT_SIZE); + + if (msgBuffer.remaining() == 0) { + log.error("MessageFormatUtil split buffer error, msg buffer length is 0"); + return bufferResultList; + } + + if (cqBuffer.remaining() == 0 || cqBuffer.remaining() % CONSUME_QUEUE_UNIT_SIZE != 0) { + log.error("MessageFormatUtil split buffer error, cq buffer size is {}", cqBuffer.remaining()); + return bufferResultList; + } + + try { + long firstCommitLogOffset = MessageFormatUtil.getCommitLogOffsetFromItem(cqBuffer); + + for (int position = cqBuffer.position(); position < cqBuffer.limit(); + position += CONSUME_QUEUE_UNIT_SIZE) { + + cqBuffer.position(position); + long logOffset = MessageFormatUtil.getCommitLogOffsetFromItem(cqBuffer); + int bufferSize = MessageFormatUtil.getSizeFromItem(cqBuffer); + long tagCode = MessageFormatUtil.getTagCodeFromItem(cqBuffer); + + int offset = (int) (logOffset - firstCommitLogOffset); + if (offset + bufferSize > msgBuffer.limit()) { + log.error("MessageFormatUtil split buffer error, message buffer offset exceeded limit. " + + "Expect length: {}, Actual length: {}", offset + bufferSize, msgBuffer.limit()); + break; + } + + msgBuffer.position(offset); + int magicCode = getMagicCode(msgBuffer); + if (magicCode == BLANK_MAGIC_CODE) { + offset += COMMIT_LOG_CODA_SIZE; + msgBuffer.position(offset); + magicCode = getMagicCode(msgBuffer); + } + if (magicCode != MessageDecoder.MESSAGE_MAGIC_CODE && + magicCode != MessageDecoder.MESSAGE_MAGIC_CODE_V2) { + log.error("MessageFormatUtil split buffer error, found unknown magic code. " + + "Message offset: {}, wrong magic code: {}", offset, magicCode); + continue; + } + + if (bufferSize != getTotalSize(msgBuffer)) { + log.error("MessageFormatUtil split buffer error, message length not match. " + + "CommitLog length: {}, buffer length: {}", getTotalSize(msgBuffer), bufferSize); + continue; + } + + ByteBuffer sliceBuffer = msgBuffer.slice(); + sliceBuffer.limit(bufferSize); + bufferResultList.add(new SelectBufferResult(sliceBuffer, offset, bufferSize, tagCode)); + } + } finally { + cqBuffer.rewind(); + msgBuffer.rewind(); + } + return bufferResultList; + } +} diff --git a/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/util/MessageStoreUtil.java b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/util/MessageStoreUtil.java new file mode 100644 index 0000000..eccde8c --- /dev/null +++ b/tieredstore/src/main/java/org/apache/rocketmq/tieredstore/util/MessageStoreUtil.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.tieredstore.util; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import org.apache.rocketmq.common.message.MessageQueue; + +public class MessageStoreUtil { + + public static final String TIERED_STORE_LOGGER_NAME = "RocketmqTieredStore"; + public static final String RMQ_SYS_TIERED_STORE_INDEX_TOPIC = "rmq_sys_INDEX"; + + public static final long BYTE = 1L; + public static final long KB = BYTE << 10; + public static final long MB = KB << 10; + public static final long GB = MB << 10; + public static final long TB = GB << 10; + public static final long PB = TB << 10; + public static final long EB = PB << 10; + + private static final DecimalFormat DEC_FORMAT = new DecimalFormat("#.##"); + + private static String formatSize(long size, long divider, String unitName) { + return DEC_FORMAT.format((double) size / divider) + unitName; + } + + public static String toHumanReadable(long size) { + if (size < 0) + return String.valueOf(size); + if (size >= EB) + return formatSize(size, EB, "EB"); + if (size >= PB) + return formatSize(size, PB, "PB"); + if (size >= TB) + return formatSize(size, TB, "TB"); + if (size >= GB) + return formatSize(size, GB, "GB"); + if (size >= MB) + return formatSize(size, MB, "MB"); + if (size >= KB) + return formatSize(size, KB, "KB"); + return formatSize(size, BYTE, "B"); + } + + public static String getHash(String str) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(str.getBytes(StandardCharsets.UTF_8)); + byte[] digest = md.digest(); + return String.format("%032x", new BigInteger(1, digest)).substring(0, 8); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + public static String toFilePath(MessageQueue mq) { + return String.format("%s/%s/%s", mq.getBrokerName(), mq.getTopic(), mq.getQueueId()); + } + + public static String getIndexFilePath(String brokerName) { + return toFilePath(new MessageQueue(RMQ_SYS_TIERED_STORE_INDEX_TOPIC, brokerName, 0)); + } + + public static String offset2FileName(final long offset) { + final NumberFormat numberFormat = NumberFormat.getInstance(); + numberFormat.setMinimumIntegerDigits(20); + numberFormat.setMaximumFractionDigits(0); + numberFormat.setGroupingUsed(false); + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(Long.toString(offset).getBytes(StandardCharsets.UTF_8)); + byte[] digest = md.digest(); + String hash = String.format("%032x", new BigInteger(1, digest)).substring(0, 8); + return hash + numberFormat.format(offset); + } catch (Exception ignore) { + return numberFormat.format(offset); + } + } + + public static long fileName2Offset(final String fileName) { + return Long.parseLong(fileName.substring(fileName.length() - 20)); + } +} diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/TieredMessageStoreTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/TieredMessageStoreTest.java new file mode 100644 index 0000000..bb259ae --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/TieredMessageStoreTest.java @@ -0,0 +1,333 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tieredstore; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.Configuration; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.GetMessageStatus; +import org.apache.rocketmq.store.QueryMessageResult; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.plugin.MessageStorePluginContext; +import org.apache.rocketmq.store.queue.ConsumeQueueInterface; +import org.apache.rocketmq.store.queue.CqUnit; +import org.apache.rocketmq.tieredstore.core.MessageStoreFetcher; +import org.apache.rocketmq.tieredstore.file.FlatFileStore; +import org.apache.rocketmq.tieredstore.file.FlatMessageFile; +import org.apache.rocketmq.tieredstore.provider.PosixFileSegment; +import org.apache.rocketmq.tieredstore.util.MessageFormatUtil; +import org.apache.rocketmq.tieredstore.util.MessageFormatUtilTest; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtil; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtilTest; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +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.eq; +import static org.mockito.Mockito.when; + +public class TieredMessageStoreTest { + + private final String brokerName = "brokerName"; + private final String storePath = MessageStoreUtilTest.getRandomStorePath(); + private final MessageQueue mq = new MessageQueue("MessageStoreTest", brokerName, 0); + + private Configuration configuration; + private DefaultMessageStore defaultStore; + private TieredMessageStore currentStore; + private FlatFileStore flatFileStore; + private MessageStoreFetcher fetcher; + + @Before + public void init() throws Exception { + BrokerConfig brokerConfig = new BrokerConfig(); + brokerConfig.setBrokerName(brokerName); + + Properties properties = new Properties(); + properties.setProperty("recordGetMessageResult", Boolean.TRUE.toString().toLowerCase(Locale.ROOT)); + properties.setProperty("tieredBackendServiceProvider", PosixFileSegment.class.getName()); + + configuration = new Configuration(LoggerFactory.getLogger( + MessageStoreUtil.TIERED_STORE_LOGGER_NAME), storePath + File.separator + "conf", + new org.apache.rocketmq.tieredstore.MessageStoreConfig(), brokerConfig); + configuration.registerConfig(properties); + + MessageStorePluginContext context = new MessageStorePluginContext( + new MessageStoreConfig(), null, null, brokerConfig, configuration); + + defaultStore = Mockito.mock(DefaultMessageStore.class); + Mockito.when(defaultStore.load()).thenReturn(true); + + currentStore = new TieredMessageStore(context, defaultStore); + Assert.assertNotNull(currentStore.getStoreConfig()); + Assert.assertNotNull(currentStore.getBrokerName()); + Assert.assertEquals(defaultStore, currentStore.getDefaultStore()); + Assert.assertNotNull(currentStore.getMetadataStore()); + Assert.assertNotNull(currentStore.getTopicFilter()); + Assert.assertNotNull(currentStore.getStoreExecutor()); + Assert.assertNotNull(currentStore.getFlatFileStore()); + Assert.assertNotNull(currentStore.getIndexService()); + + fetcher = Mockito.spy(currentStore.fetcher); + try { + Field field = currentStore.getClass().getDeclaredField("fetcher"); + field.setAccessible(true); + field.set(currentStore, fetcher); + } catch (NoSuchFieldException | IllegalAccessException e) { + Assert.fail(e.getClass().getCanonicalName() + ": " + e.getMessage()); + } + + flatFileStore = currentStore.getFlatFileStore(); + + Mockito.when(defaultStore.getMinOffsetInQueue(anyString(), anyInt())).thenReturn(100L); + Mockito.when(defaultStore.getMaxOffsetInQueue(anyString(), anyInt())).thenReturn(200L); + ConsumeQueueInterface cq = Mockito.mock(ConsumeQueueInterface.class); + Mockito.when(defaultStore.getConsumeQueue(anyString(), anyInt())).thenReturn(cq); + + ByteBuffer buffer = MessageFormatUtilTest.buildMockedMessageBuffer(); + Mockito.when(cq.get(anyLong())).thenReturn( + new CqUnit(100, 1000, buffer.remaining(), 0L)); + Mockito.when(defaultStore.selectOneMessageByOffset(anyLong(), anyInt())).thenReturn( + new SelectMappedBufferResult(0L, buffer.asReadOnlyBuffer(), buffer.remaining(), null)); + currentStore.load(); + + FlatMessageFile flatFile = currentStore.getFlatFileStore().computeIfAbsent(mq); + Assert.assertNotNull(flatFile); + currentStore.dispatcher.doScheduleDispatch(flatFile, true).join(); + + for (int i = 100; i < 200; i++) { + SelectMappedBufferResult bufferResult = new SelectMappedBufferResult( + 0L, buffer, buffer.remaining(), null); + DispatchRequest request = new DispatchRequest(mq.getTopic(), mq.getQueueId(), + MessageFormatUtil.getCommitLogOffset(buffer), buffer.remaining(), 0L, + MessageFormatUtil.getStoreTimeStamp(buffer), 0L, + "", "", 0, 0L, new HashMap<>()); + flatFile.appendCommitLog(bufferResult); + flatFile.appendConsumeQueue(request); + } + currentStore.dispatcher.doScheduleDispatch(flatFile, true).join(); + } + + @After + public void shutdown() throws IOException { + currentStore.shutdown(); + currentStore.destroy(); + MessageStoreUtilTest.deleteStoreDirectory(storePath); + } + + @Test + public void testViaTieredStorage() { + Properties properties = new Properties(); + + // TieredStorageLevel.DISABLE + properties.setProperty("tieredStorageLevel", "0"); + configuration.update(properties); + Assert.assertFalse(currentStore.fetchFromCurrentStore(mq.getTopic(), mq.getQueueId(), 0)); + + // TieredStorageLevel.NOT_IN_DISK + properties.setProperty("tieredStorageLevel", "1"); + configuration.update(properties); + when(defaultStore.checkInStoreByConsumeOffset(anyString(), anyInt(), anyLong())).thenReturn(false); + Assert.assertTrue(currentStore.fetchFromCurrentStore(mq.getTopic(), mq.getQueueId(), 0)); + + when(defaultStore.checkInStoreByConsumeOffset(anyString(), anyInt(), anyLong())).thenReturn(true); + Assert.assertFalse(currentStore.fetchFromCurrentStore(mq.getTopic(), mq.getQueueId(), 0)); + + // TieredStorageLevel.NOT_IN_MEM + properties.setProperty("tieredStorageLevel", "2"); + configuration.update(properties); + Mockito.when(defaultStore.checkInStoreByConsumeOffset(anyString(), anyInt(), anyLong())).thenReturn(false); + Mockito.when(defaultStore.checkInMemByConsumeOffset(anyString(), anyInt(), anyLong(), anyInt())).thenReturn(true); + Assert.assertTrue(currentStore.fetchFromCurrentStore(mq.getTopic(), mq.getQueueId(), 0)); + + Mockito.when(defaultStore.checkInStoreByConsumeOffset(anyString(), anyInt(), anyLong())).thenReturn(true); + Mockito.when(defaultStore.checkInMemByConsumeOffset(anyString(), anyInt(), anyLong(), anyInt())).thenReturn(false); + Assert.assertTrue(currentStore.fetchFromCurrentStore(mq.getTopic(), mq.getQueueId(), 0)); + + Mockito.when(defaultStore.checkInStoreByConsumeOffset(anyString(), anyInt(), anyLong())).thenReturn(true); + Mockito.when(defaultStore.checkInMemByConsumeOffset(anyString(), anyInt(), anyLong(), anyInt())).thenReturn(true); + Assert.assertFalse(currentStore.fetchFromCurrentStore(mq.getTopic(), mq.getQueueId(), 0)); + + // TieredStorageLevel.FORCE + properties.setProperty("tieredStorageLevel", "3"); + configuration.update(properties); + Assert.assertTrue(currentStore.fetchFromCurrentStore(mq.getTopic(), mq.getQueueId(), 0)); + } + + @Test + public void testGetMessageAsync() { + GetMessageResult expect = new GetMessageResult(); + expect.setStatus(GetMessageStatus.FOUND); + expect.setMinOffset(100L); + expect.setMaxOffset(200L); + + // topic filter + Mockito.when(defaultStore.getMessageAsync(anyString(), anyString(), anyInt(), anyLong(), anyInt(), any())) + .thenReturn(CompletableFuture.completedFuture(expect)); + String groupName = "groupName"; + GetMessageResult result = currentStore.getMessage( + groupName, TopicValidator.SYSTEM_TOPIC_PREFIX, mq.getQueueId(), 100, 0, null); + Assert.assertSame(expect, result); + + // fetch from default + Mockito.when(fetcher.getMessageAsync(anyString(), anyString(), anyInt(), anyLong(), anyInt(), any())) + .thenReturn(CompletableFuture.completedFuture(expect)); + + result = currentStore.getMessage( + groupName, mq.getTopic(), mq.getQueueId(), 100, 0, null); + Assert.assertSame(expect, result); + + expect.setStatus(GetMessageStatus.NO_MATCHED_LOGIC_QUEUE); + Assert.assertSame(expect, currentStore.getMessage( + groupName, mq.getTopic(), mq.getQueueId(), 0, 0, null)); + + expect.setStatus(GetMessageStatus.OFFSET_OVERFLOW_ONE); + Assert.assertSame(expect, currentStore.getMessage( + groupName, mq.getTopic(), mq.getQueueId(), 0, 0, null)); + + expect.setStatus(GetMessageStatus.OFFSET_OVERFLOW_BADLY); + Assert.assertSame(expect, currentStore.getMessage( + groupName, mq.getTopic(), mq.getQueueId(), 0, 0, null)); + + expect.setStatus(GetMessageStatus.OFFSET_RESET); + Assert.assertSame(expect, currentStore.getMessage( + groupName, mq.getTopic(), mq.getQueueId(), 0, 0, null)); + } + + @Test + public void testGetMinOffsetInQueue() { + FlatMessageFile flatFile = flatFileStore.getFlatFile(mq); + Mockito.when(defaultStore.getMinOffsetInQueue(anyString(), anyInt())).thenReturn(100L); + Assert.assertEquals(100L, currentStore.getMinOffsetInQueue(mq.getTopic(), mq.getQueueId())); + + Mockito.when(flatFile.getConsumeQueueMinOffset()).thenReturn(10L); + Assert.assertEquals(10L, currentStore.getMinOffsetInQueue(mq.getTopic(), mq.getQueueId())); + } + + @Test + public void testGetEarliestMessageTimeAsync() { + when(fetcher.getEarliestMessageTimeAsync(anyString(), anyInt())).thenReturn(CompletableFuture.completedFuture(1L)); + Assert.assertEquals(0, (long) currentStore.getEarliestMessageTimeAsync(mq.getTopic(), mq.getQueueId()).join()); + + when(fetcher.getEarliestMessageTimeAsync(anyString(), anyInt())).thenReturn(CompletableFuture.completedFuture(-1L)); + when(defaultStore.getEarliestMessageTime(anyString(), anyInt())).thenReturn(2L); + Assert.assertEquals(2, (long) currentStore.getEarliestMessageTimeAsync(mq.getTopic(), mq.getQueueId()).join()); + } + + @Test + public void testGetMessageStoreTimeStampAsync() { + // TieredStorageLevel.DISABLE + Properties properties = new Properties(); + properties.setProperty("tieredStorageLevel", "DISABLE"); + configuration.update(properties); + when(fetcher.getMessageStoreTimeStampAsync(anyString(), anyInt(), anyLong())).thenReturn(CompletableFuture.completedFuture(1L)); + when(defaultStore.getMessageStoreTimeStampAsync(anyString(), anyInt(), anyLong())).thenReturn(CompletableFuture.completedFuture(2L)); + when(defaultStore.getMessageStoreTimeStamp(anyString(), anyInt(), anyLong())).thenReturn(3L); + Assert.assertEquals(2, (long) currentStore.getMessageStoreTimeStampAsync(mq.getTopic(), mq.getQueueId(), 0).join()); + + // TieredStorageLevel.FORCE + properties.setProperty("tieredStorageLevel", "FORCE"); + configuration.update(properties); + Assert.assertEquals(1, (long) currentStore.getMessageStoreTimeStampAsync(mq.getTopic(), mq.getQueueId(), 0).join()); + + Mockito.when(fetcher.getMessageStoreTimeStampAsync(anyString(), anyInt(), anyLong())).thenReturn(CompletableFuture.completedFuture(-1L)); + Assert.assertEquals(3, (long) currentStore.getMessageStoreTimeStampAsync(mq.getTopic(), mq.getQueueId(), 0).join()); + } + + @Test + public void testGetOffsetInQueueByTime() { + Properties properties = new Properties(); + properties.setProperty("tieredStorageLevel", "FORCE"); + configuration.update(properties); + + Mockito.when(fetcher.getOffsetInQueueByTime(anyString(), anyInt(), anyLong(), eq(BoundaryType.LOWER))).thenReturn(1L); + Mockito.when(defaultStore.getOffsetInQueueByTime(anyString(), anyInt(), anyLong())).thenReturn(2L); + Mockito.when(defaultStore.getEarliestMessageTime()).thenReturn(100L); + Assert.assertEquals(1L, currentStore.getOffsetInQueueByTime(mq.getTopic(), mq.getQueueId(), 1000, BoundaryType.LOWER)); + Assert.assertEquals(1L, currentStore.getOffsetInQueueByTime(mq.getTopic(), mq.getQueueId(), 0, BoundaryType.LOWER)); + + Mockito.when(fetcher.getOffsetInQueueByTime(anyString(), anyInt(), anyLong(), eq(BoundaryType.LOWER))).thenReturn(-1L); + Assert.assertEquals(-1L, currentStore.getOffsetInQueueByTime(mq.getTopic(), mq.getQueueId(), 0)); + Assert.assertEquals(-1L, currentStore.getOffsetInQueueByTime(mq.getTopic(), mq.getQueueId(), 0, BoundaryType.LOWER)); + } + + @Test + public void testQueryMessage() { + QueryMessageResult result1 = new QueryMessageResult(); + result1.addMessage(new SelectMappedBufferResult(0, null, 0, null)); + result1.addMessage(new SelectMappedBufferResult(0, null, 0, null)); + when(fetcher.queryMessageAsync(anyString(), anyString(), anyInt(), anyLong(), anyLong())).thenReturn(CompletableFuture.completedFuture(result1)); + QueryMessageResult result2 = new QueryMessageResult(); + result2.addMessage(new SelectMappedBufferResult(0, null, 0, null)); + when(defaultStore.queryMessage(anyString(), anyString(), anyInt(), anyLong(), anyLong())).thenReturn(result2); + when(defaultStore.getEarliestMessageTime()).thenReturn(100L); + Assert.assertEquals(2, currentStore.queryMessage(mq.getTopic(), "key", 32, 0, 99).getMessageMapedList().size()); + Assert.assertEquals(1, currentStore.queryMessage(mq.getTopic(), "key", 32, 100, 200).getMessageMapedList().size()); + Assert.assertEquals(3, currentStore.queryMessage(mq.getTopic(), "key", 32, 0, 200).getMessageMapedList().size()); + } + + @Test + public void testCleanUnusedTopics() { + Set topicSet = new HashSet<>(); + currentStore.cleanUnusedTopic(topicSet); + Assert.assertNull(flatFileStore.getFlatFile(mq)); + Assert.assertNull(flatFileStore.getMetadataStore().getTopic(mq.getTopic())); + Assert.assertNull(flatFileStore.getMetadataStore().getQueue(mq)); + } + + @Test + public void testDeleteTopics() { + Set topicSet = new HashSet<>(); + topicSet.add(mq.getTopic()); + currentStore.deleteTopics(topicSet); + Assert.assertNull(flatFileStore.getFlatFile(mq)); + Assert.assertNull(flatFileStore.getMetadataStore().getTopic(mq.getTopic())); + Assert.assertNull(flatFileStore.getMetadataStore().getQueue(mq)); + } + + @Test + public void testMetrics() { + currentStore.getMetricsView(); + currentStore.initMetrics( + OpenTelemetrySdk.builder().build().getMeter(""), Attributes::builder); + } +} diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/common/FileSegmentTypeTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/common/FileSegmentTypeTest.java new file mode 100644 index 0000000..28439e0 --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/common/FileSegmentTypeTest.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.tieredstore.common; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class FileSegmentTypeTest { + + @Test + public void getTypeCodeTest() { + assertEquals(0, FileSegmentType.COMMIT_LOG.getCode()); + assertEquals(1, FileSegmentType.CONSUME_QUEUE.getCode()); + assertEquals(2, FileSegmentType.INDEX.getCode()); + } + + @Test + public void getTypeFromValueTest() { + assertEquals(FileSegmentType.COMMIT_LOG, FileSegmentType.valueOf(0)); + assertEquals(FileSegmentType.CONSUME_QUEUE, FileSegmentType.valueOf(1)); + assertEquals(FileSegmentType.INDEX, FileSegmentType.valueOf(2)); + } +} \ No newline at end of file diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/common/GetMessageResultExtTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/common/GetMessageResultExtTest.java new file mode 100644 index 0000000..69240a4 --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/common/GetMessageResultExtTest.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.tieredstore.common; + +import java.nio.ByteBuffer; +import java.util.Map; +import org.apache.rocketmq.store.ConsumeQueueExt; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.GetMessageStatus; +import org.apache.rocketmq.store.MessageFilter; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.tieredstore.util.MessageFormatUtilTest; +import org.junit.Assert; +import org.junit.Test; + +public class GetMessageResultExtTest { + + @Test + public void doFilterTest() { + GetMessageResultExt resultExt = new GetMessageResultExt(); + Assert.assertNull(resultExt.getStatus()); + Assert.assertEquals(0, resultExt.doFilterMessage(null).getMessageCount()); + + resultExt.setStatus(GetMessageStatus.OFFSET_OVERFLOW_ONE); + Assert.assertEquals(0, resultExt.doFilterMessage(null).getMessageCount()); + + resultExt.setStatus(GetMessageStatus.OFFSET_OVERFLOW_BADLY); + Assert.assertEquals(0, resultExt.doFilterMessage(null).getMessageCount()); + + int total = 3; + for (int i = 0; i < total; i++) { + resultExt.addMessageExt(new SelectMappedBufferResult(i * 1000L, + MessageFormatUtilTest.buildMockedMessageBuffer(), 1000, null), + 0, ("Tag" + i).hashCode()); + } + Assert.assertEquals(total, resultExt.getMessageCount()); + Assert.assertEquals(total, resultExt.getTagCodeList().size()); + + resultExt.setStatus(GetMessageStatus.FOUND); + GetMessageResult getMessageResult = resultExt.doFilterMessage(new MessageFilter() { + @Override + public boolean isMatchedByConsumeQueue(Long tagsCode, ConsumeQueueExt.CqExtUnit cqExtUnit) { + return false; + } + + @Override + public boolean isMatchedByCommitLog(ByteBuffer msgBuffer, Map properties) { + return false; + } + }); + Assert.assertEquals(0, getMessageResult.getMessageCount()); + + getMessageResult = resultExt.doFilterMessage(new MessageFilter() { + @Override + public boolean isMatchedByConsumeQueue(Long tagsCode, ConsumeQueueExt.CqExtUnit cqExtUnit) { + return "Tag1".hashCode() == tagsCode; + } + + @Override + public boolean isMatchedByCommitLog(ByteBuffer msgBuffer, Map properties) { + return false; + } + }); + Assert.assertEquals(0, getMessageResult.getMessageCount()); + + getMessageResult = resultExt.doFilterMessage(new MessageFilter() { + @Override + public boolean isMatchedByConsumeQueue(Long tagsCode, ConsumeQueueExt.CqExtUnit cqExtUnit) { + return "Tag1".hashCode() == tagsCode; + } + + @Override + public boolean isMatchedByCommitLog(ByteBuffer msgBuffer, Map properties) { + return true; + } + }); + Assert.assertEquals(1, getMessageResult.getMessageCount()); + } +} \ No newline at end of file diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/common/GroupCommitContextTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/common/GroupCommitContextTest.java new file mode 100644 index 0000000..e692360 --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/common/GroupCommitContextTest.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.tieredstore.common; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.junit.Assert; +import org.junit.Test; + +public class GroupCommitContextTest { + + @Test + public void groupCommitContextTest() { + GroupCommitContext releaseGroupCommitContext = new GroupCommitContext(); + releaseGroupCommitContext.release(); + + long endOffset = 1000; + List dispatchRequestList = new ArrayList<>(); + dispatchRequestList.add(new DispatchRequest(1000)); + List selectMappedBufferResultList = new ArrayList<>(); + selectMappedBufferResultList.add(new SelectMappedBufferResult(100, ByteBuffer.allocate(10), 1000, null)); + GroupCommitContext groupCommitContext = new GroupCommitContext(); + groupCommitContext.setEndOffset(endOffset); + groupCommitContext.setBufferList(selectMappedBufferResultList); + groupCommitContext.setDispatchRequests(dispatchRequestList); + + Assert.assertTrue(groupCommitContext.getEndOffset() == endOffset); + Assert.assertTrue(groupCommitContext.getBufferList().equals(selectMappedBufferResultList)); + Assert.assertTrue(groupCommitContext.getDispatchRequests().equals(dispatchRequestList)); + groupCommitContext.release(); + Assert.assertTrue(groupCommitContext.getDispatchRequests() == null); + Assert.assertTrue(groupCommitContext.getBufferList() == null); + Assert.assertTrue(dispatchRequestList.isEmpty()); + Assert.assertTrue(selectMappedBufferResultList.isEmpty()); + } + +} diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/common/SelectBufferResultTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/common/SelectBufferResultTest.java new file mode 100644 index 0000000..f9dfce9 --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/common/SelectBufferResultTest.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.tieredstore.common; + +import java.nio.ByteBuffer; +import org.junit.Assert; +import org.junit.Test; + +public class SelectBufferResultTest { + + @Test + public void selectBufferResultTest() { + ByteBuffer buffer = ByteBuffer.allocate(10); + long startOffset = 5L; + int size = 10; + long tagCode = 1L; + + SelectBufferResult result = new SelectBufferResult(buffer, startOffset, size, tagCode); + Assert.assertEquals(buffer, result.getByteBuffer()); + Assert.assertEquals(startOffset, result.getStartOffset()); + Assert.assertEquals(size, result.getSize()); + Assert.assertEquals(tagCode, result.getTagCode()); + } +} \ No newline at end of file diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/core/MessageStoreDispatcherImplTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/core/MessageStoreDispatcherImplTest.java new file mode 100644 index 0000000..7a43e1e --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/core/MessageStoreDispatcherImplTest.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.tieredstore.core; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +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.store.DispatchRequest; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.queue.ConsumeQueueInterface; +import org.apache.rocketmq.store.queue.CqUnit; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.MessageStoreExecutor; +import org.apache.rocketmq.tieredstore.TieredMessageStore; +import org.apache.rocketmq.tieredstore.common.GroupCommitContext; +import org.apache.rocketmq.tieredstore.file.FlatFileFactory; +import org.apache.rocketmq.tieredstore.file.FlatFileStore; +import org.apache.rocketmq.tieredstore.file.FlatMessageFile; +import org.apache.rocketmq.tieredstore.index.IndexItem; +import org.apache.rocketmq.tieredstore.index.IndexService; +import org.apache.rocketmq.tieredstore.index.IndexStoreService; +import org.apache.rocketmq.tieredstore.metadata.DefaultMetadataStore; +import org.apache.rocketmq.tieredstore.metadata.MetadataStore; +import org.apache.rocketmq.tieredstore.provider.PosixFileSegment; +import org.apache.rocketmq.tieredstore.util.MessageFormatUtil; +import org.apache.rocketmq.tieredstore.util.MessageFormatUtilTest; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtilTest; +import org.awaitility.Awaitility; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; + +public class MessageStoreDispatcherImplTest { + + protected final String storePath = MessageStoreUtilTest.getRandomStorePath(); + protected MessageQueue mq; + protected MetadataStore metadataStore; + protected MessageStoreConfig storeConfig; + protected MessageStoreExecutor executor; + protected FlatFileStore fileStore; + protected TieredMessageStore messageStore; + + @Before + public void init() { + storeConfig = new MessageStoreConfig(); + storeConfig.setBrokerName("brokerName"); + storeConfig.setStorePathRootDir(storePath); + storeConfig.setTieredStoreFilePath(storePath); + storeConfig.setTieredBackendServiceProvider(PosixFileSegment.class.getName()); + mq = new MessageQueue("StoreTest", storeConfig.getBrokerName(), 1); + metadataStore = new DefaultMetadataStore(storeConfig); + executor = new MessageStoreExecutor(); + fileStore = new FlatFileStore(storeConfig, metadataStore, executor); + } + + @After + public void shutdown() throws IOException { + if (messageStore != null) { + messageStore.destroy(); + } + MessageStoreUtilTest.deleteStoreDirectory(storePath); + } + + @Test + public void dispatchFromCommitLogTest() throws Exception { + MessageStore defaultStore = Mockito.mock(MessageStore.class); + Mockito.when(defaultStore.getMinOffsetInQueue(anyString(), anyInt())).thenReturn(100L); + Mockito.when(defaultStore.getMaxOffsetInQueue(anyString(), anyInt())).thenReturn(200L); + + messageStore = Mockito.mock(TieredMessageStore.class); + IndexService indexService = + new IndexStoreService(new FlatFileFactory(metadataStore, storeConfig), storePath); + indexService.start(); + Mockito.when(messageStore.getDefaultStore()).thenReturn(defaultStore); + Mockito.when(messageStore.getStoreConfig()).thenReturn(storeConfig); + Mockito.when(messageStore.getStoreExecutor()).thenReturn(executor); + Mockito.when(messageStore.getFlatFileStore()).thenReturn(fileStore); + Mockito.when(messageStore.getIndexService()).thenReturn(indexService); + Mockito.when(messageStore.getMessageStoreConfig()).thenReturn(new org.apache.rocketmq.store.config.MessageStoreConfig()); + + // mock message + ByteBuffer buffer = MessageFormatUtilTest.buildMockedMessageBuffer(); + MessageExt messageExt = MessageDecoder.decode(buffer); + messageExt.setKeys("Key"); + MessageAccessor.putProperty( + messageExt, MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, "uk"); + messageExt.setBody(new byte[10]); + messageExt.setStoreSize(0); + buffer = ByteBuffer.wrap(MessageDecoder.encode(messageExt, false)); + buffer.putInt(0, buffer.remaining()); + + DispatchRequest request = new DispatchRequest(mq.getTopic(), mq.getQueueId(), + MessageFormatUtil.getCommitLogOffset(buffer), buffer.remaining(), 0L, + MessageFormatUtil.getStoreTimeStamp(buffer), 0L, + "", "", 0, 0L, new HashMap<>()); + + // construct flat file + MessageStoreDispatcher dispatcher = new MessageStoreDispatcherImpl(messageStore); + dispatcher.dispatch(request); + FlatMessageFile flatFile = fileStore.getFlatFile(mq); + Assert.assertNotNull(flatFile); + + // init offset + dispatcher.doScheduleDispatch(flatFile, true).join(); + Assert.assertEquals(100L, flatFile.getConsumeQueueMinOffset()); + Assert.assertEquals(100L, flatFile.getConsumeQueueMaxOffset()); + Assert.assertEquals(100L, flatFile.getConsumeQueueCommitOffset()); + + ConsumeQueueInterface cq = Mockito.mock(ConsumeQueueInterface.class); + Mockito.when(defaultStore.getConsumeQueue(anyString(), anyInt())).thenReturn(cq); + Mockito.when(cq.get(anyLong())).thenReturn( + new CqUnit(100, 1000, buffer.remaining(), 0L)); + Mockito.when(defaultStore.selectOneMessageByOffset(anyLong(), anyInt())).thenReturn( + new SelectMappedBufferResult(0L, buffer.asReadOnlyBuffer(), buffer.remaining(), null)); + dispatcher.doScheduleDispatch(flatFile, true).join(); + + Awaitility.await().pollInterval(Duration.ofSeconds(1)).atMost(Duration.ofSeconds(30)).until(() -> { + List resultList1 = indexService.queryAsync( + mq.getTopic(), "uk", 32, 0L, System.currentTimeMillis()).join(); + List resultList2 = indexService.queryAsync( + mq.getTopic(), "uk", 120, 0L, System.currentTimeMillis()).join(); + Assert.assertEquals(32, resultList1.size()); + Assert.assertEquals(100, resultList2.size()); + return true; + }); + + Assert.assertEquals(100L, flatFile.getConsumeQueueMinOffset()); + Assert.assertEquals(200L, flatFile.getConsumeQueueMaxOffset()); + Assert.assertEquals(200L, flatFile.getConsumeQueueCommitOffset()); + } + + @Test + public void dispatchCommitFailedTest() throws Exception { + MessageStore defaultStore = Mockito.mock(MessageStore.class); + Mockito.when(defaultStore.getMinOffsetInQueue(anyString(), anyInt())).thenReturn(100L); + Mockito.when(defaultStore.getMaxOffsetInQueue(anyString(), anyInt())).thenReturn(200L); + + messageStore = Mockito.mock(TieredMessageStore.class); + IndexService indexService = + new IndexStoreService(new FlatFileFactory(metadataStore, storeConfig), storePath); + indexService.start(); + Mockito.when(messageStore.getDefaultStore()).thenReturn(defaultStore); + Mockito.when(messageStore.getStoreConfig()).thenReturn(storeConfig); + Mockito.when(messageStore.getStoreExecutor()).thenReturn(executor); + Mockito.when(messageStore.getFlatFileStore()).thenReturn(fileStore); + Mockito.when(messageStore.getIndexService()).thenReturn(indexService); + + // mock message + ByteBuffer buffer = MessageFormatUtilTest.buildMockedMessageBuffer(); + MessageExt messageExt = MessageDecoder.decode(buffer); + messageExt.setKeys("Key"); + MessageAccessor.putProperty( + messageExt, MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, "uk"); + messageExt.setBody(new byte[10]); + messageExt.setStoreSize(0); + buffer = ByteBuffer.wrap(MessageDecoder.encode(messageExt, false)); + buffer.putInt(0, buffer.remaining()); + + DispatchRequest request = new DispatchRequest(mq.getTopic(), mq.getQueueId(), + MessageFormatUtil.getCommitLogOffset(buffer), buffer.remaining(), 0L, + MessageFormatUtil.getStoreTimeStamp(buffer), 0L, + "", "", 0, 0L, new HashMap<>()); + + // construct flat file + MessageStoreDispatcher dispatcher = new MessageStoreDispatcherImpl(messageStore); + dispatcher.dispatch(request); + FlatMessageFile flatFile = fileStore.getFlatFile(mq); + Assert.assertNotNull(flatFile); + + // init offset + dispatcher.doScheduleDispatch(flatFile, true).join(); + Assert.assertEquals(100L, flatFile.getConsumeQueueMinOffset()); + Assert.assertEquals(100L, flatFile.getConsumeQueueMaxOffset()); + Assert.assertEquals(100L, flatFile.getConsumeQueueCommitOffset()); + + ConsumeQueueInterface cq = Mockito.mock(ConsumeQueueInterface.class); + Mockito.when(defaultStore.getConsumeQueue(anyString(), anyInt())).thenReturn(cq); + Mockito.when(cq.get(anyLong())).thenReturn( + new CqUnit(100, 1000, buffer.remaining(), 0L)); + Mockito.when(defaultStore.selectOneMessageByOffset(anyLong(), anyInt())).thenReturn( + new SelectMappedBufferResult(0L, buffer.asReadOnlyBuffer(), buffer.remaining(), null)); + flatFile.getCommitLock().drainPermits(); + dispatcher.doScheduleDispatch(flatFile, true).join(); + GroupCommitContext groupCommitContext = ((MessageStoreDispatcherImpl)dispatcher).getFailedGroupCommitMap().get(flatFile); + Assert.assertTrue(groupCommitContext != null); + Assert.assertTrue(groupCommitContext.getEndOffset() == 200); + flatFile.getCommitLock().release(); + flatFile.commitAsync().join(); + dispatcher.doScheduleDispatch(flatFile, true).join(); + Assert.assertTrue(((MessageStoreDispatcherImpl)dispatcher).getFailedGroupCommitMap().get(flatFile) == null); + ((MessageStoreDispatcherImpl)dispatcher).flatFileStore.destroyFile(mq); + ((MessageStoreDispatcherImpl)dispatcher).releaseClosedPendingGroupCommit(); + + } + + @Test + public void dispatchFailedGroupCommitMapReleaseTest() throws Exception { + MessageStore defaultStore = Mockito.mock(MessageStore.class); + Mockito.when(defaultStore.getMinOffsetInQueue(anyString(), anyInt())).thenReturn(100L); + Mockito.when(defaultStore.getMaxOffsetInQueue(anyString(), anyInt())).thenReturn(200L); + + messageStore = Mockito.mock(TieredMessageStore.class); + IndexService indexService = + new IndexStoreService(new FlatFileFactory(metadataStore, storeConfig), storePath); + indexService.start(); + Mockito.when(messageStore.getDefaultStore()).thenReturn(defaultStore); + Mockito.when(messageStore.getStoreConfig()).thenReturn(storeConfig); + Mockito.when(messageStore.getStoreExecutor()).thenReturn(executor); + Mockito.when(messageStore.getFlatFileStore()).thenReturn(fileStore); + Mockito.when(messageStore.getIndexService()).thenReturn(indexService); + + // mock message + ByteBuffer buffer = MessageFormatUtilTest.buildMockedMessageBuffer(); + MessageExt messageExt = MessageDecoder.decode(buffer); + messageExt.setKeys("Key"); + MessageAccessor.putProperty( + messageExt, MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, "uk"); + messageExt.setBody(new byte[10]); + messageExt.setStoreSize(0); + buffer = ByteBuffer.wrap(MessageDecoder.encode(messageExt, false)); + buffer.putInt(0, buffer.remaining()); + + DispatchRequest request = new DispatchRequest(mq.getTopic(), mq.getQueueId(), + MessageFormatUtil.getCommitLogOffset(buffer), buffer.remaining(), 0L, + MessageFormatUtil.getStoreTimeStamp(buffer), 0L, + "", "", 0, 0L, new HashMap<>()); + + // construct flat file + MessageStoreDispatcher dispatcher = new MessageStoreDispatcherImpl(messageStore); + dispatcher.dispatch(request); + FlatMessageFile flatFile = fileStore.getFlatFile(mq); + Assert.assertNotNull(flatFile); + + // init offset + dispatcher.doScheduleDispatch(flatFile, true).join(); + Assert.assertEquals(100L, flatFile.getConsumeQueueMinOffset()); + Assert.assertEquals(100L, flatFile.getConsumeQueueMaxOffset()); + Assert.assertEquals(100L, flatFile.getConsumeQueueCommitOffset()); + + ConsumeQueueInterface cq = Mockito.mock(ConsumeQueueInterface.class); + Mockito.when(defaultStore.getConsumeQueue(anyString(), anyInt())).thenReturn(cq); + Mockito.when(cq.get(anyLong())).thenReturn( + new CqUnit(100, 1000, buffer.remaining(), 0L)); + Mockito.when(defaultStore.selectOneMessageByOffset(anyLong(), anyInt())).thenReturn( + new SelectMappedBufferResult(0L, buffer.asReadOnlyBuffer(), buffer.remaining(), null)); + flatFile.getCommitLock().drainPermits(); + dispatcher.doScheduleDispatch(flatFile, true).join(); + GroupCommitContext groupCommitContext = ((MessageStoreDispatcherImpl)dispatcher).getFailedGroupCommitMap().get(flatFile); + Assert.assertTrue(groupCommitContext != null); + ((MessageStoreDispatcherImpl)dispatcher).flatFileStore.destroyFile(mq); + ((MessageStoreDispatcherImpl)dispatcher).releaseClosedPendingGroupCommit(); + Assert.assertTrue(((MessageStoreDispatcherImpl)dispatcher).getFailedGroupCommitMap().get(flatFile) == null); + + } + + @Test + public void dispatchServiceTest() { + MessageStore defaultStore = Mockito.mock(MessageStore.class); + messageStore = Mockito.mock(TieredMessageStore.class); + IndexService indexService = + new IndexStoreService(new FlatFileFactory(metadataStore, storeConfig), storePath); + Mockito.when(messageStore.getDefaultStore()).thenReturn(defaultStore); + Mockito.when(messageStore.getStoreConfig()).thenReturn(storeConfig); + Mockito.when(messageStore.getStoreExecutor()).thenReturn(executor); + Mockito.when(messageStore.getFlatFileStore()).thenReturn(fileStore); + Mockito.when(messageStore.getIndexService()).thenReturn(indexService); + + // construct flat file + ByteBuffer buffer = MessageFormatUtilTest.buildMockedMessageBuffer(); + DispatchRequest request = new DispatchRequest(mq.getTopic(), mq.getQueueId(), + MessageFormatUtil.getCommitLogOffset(buffer), buffer.remaining(), 0L, + MessageFormatUtil.getStoreTimeStamp(buffer), 0L, + "", "", 0, 0L, new HashMap<>()); + MessageStoreDispatcherImpl dispatcher = new MessageStoreDispatcherImpl(messageStore); + dispatcher.dispatch(request); + FlatMessageFile flatFile = fileStore.getFlatFile(mq); + Assert.assertNotNull(flatFile); + + AtomicBoolean result = new AtomicBoolean(false); + MessageStoreDispatcherImpl dispatcherSpy = Mockito.spy(dispatcher); + Mockito.doAnswer(mock -> { + result.set(true); + return true; + }).when(dispatcherSpy).dispatchWithSemaphore(any()); + dispatcherSpy.start(); + Awaitility.await().atMost(Duration.ofSeconds(10)).until(result::get); + dispatcherSpy.shutdown(); + } +} diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/core/MessageStoreFetcherImplTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/core/MessageStoreFetcherImplTest.java new file mode 100644 index 0000000..fdcdec0 --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/core/MessageStoreFetcherImplTest.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. + */ +package org.apache.rocketmq.tieredstore.core; + +import com.google.common.collect.Sets; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.store.DefaultMessageFilter; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.GetMessageStatus; +import org.apache.rocketmq.store.MessageFilter; +import org.apache.rocketmq.store.QueryMessageResult; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.TieredMessageStore; +import org.apache.rocketmq.tieredstore.common.SelectBufferResult; +import org.apache.rocketmq.tieredstore.file.FlatMessageFile; +import org.apache.rocketmq.tieredstore.util.MessageFormatUtilTest; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtilTest; +import org.awaitility.Awaitility; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import static org.apache.rocketmq.tieredstore.core.MessageStoreFetcherImpl.CACHE_KEY_FORMAT; + +public class MessageStoreFetcherImplTest { + + private String groupName; + private MessageQueue mq; + private MessageStoreConfig storeConfig; + private TieredMessageStore messageStore; + private MessageStoreDispatcherImplTest dispatcherTest; + private MessageStoreFetcherImpl fetcher; + + @Before + public void init() throws Exception { + groupName = "GID-fetcherTest"; + dispatcherTest = new MessageStoreDispatcherImplTest(); + dispatcherTest.init(); + } + + @After + public void shutdown() throws IOException { + if (messageStore != null) { + messageStore.destroy(); + } + MessageStoreUtilTest.deleteStoreDirectory(dispatcherTest.storePath); + } + + @Test + public void getMessageFromTieredStoreTest() throws Exception { + dispatcherTest.dispatchFromCommitLogTest(); + mq = dispatcherTest.mq; + messageStore = dispatcherTest.messageStore; + storeConfig = dispatcherTest.storeConfig; + + storeConfig.setReadAheadCacheEnable(true); + fetcher = new MessageStoreFetcherImpl(messageStore); + GetMessageResult getMessageResult = fetcher.getMessageAsync( + groupName, mq.getTopic(), 0, 0, 32, null).join(); + Assert.assertEquals(GetMessageStatus.NO_MATCHED_LOGIC_QUEUE, getMessageResult.getStatus()); + + getMessageResult = fetcher.getMessageAsync( + groupName, mq.getTopic(), mq.getQueueId(), 0, 32, null).join(); + Assert.assertEquals(GetMessageStatus.OFFSET_TOO_SMALL, getMessageResult.getStatus()); + Assert.assertEquals(100L, getMessageResult.getMinOffset()); + Assert.assertEquals(200L, getMessageResult.getMaxOffset()); + Assert.assertEquals(100L, getMessageResult.getNextBeginOffset()); + + getMessageResult = fetcher.getMessageAsync( + groupName, mq.getTopic(), mq.getQueueId(), 200, 32, null).join(); + Assert.assertEquals(GetMessageStatus.OFFSET_OVERFLOW_ONE, getMessageResult.getStatus()); + Assert.assertEquals(100L, getMessageResult.getMinOffset()); + Assert.assertEquals(200L, getMessageResult.getMaxOffset()); + Assert.assertEquals(200L, getMessageResult.getNextBeginOffset()); + + getMessageResult = fetcher.getMessageAsync( + groupName, mq.getTopic(), mq.getQueueId(), 300, 32, null).join(); + Assert.assertEquals(GetMessageStatus.OFFSET_OVERFLOW_BADLY, getMessageResult.getStatus()); + Assert.assertEquals(100L, getMessageResult.getMinOffset()); + Assert.assertEquals(200L, getMessageResult.getMaxOffset()); + Assert.assertEquals(200L, getMessageResult.getNextBeginOffset()); + + FlatMessageFile flatFile = dispatcherTest.fileStore.getFlatFile(mq); + + // direct + getMessageResult = fetcher.getMessageFromTieredStoreAsync(flatFile, 0, 32).join(); + Assert.assertEquals(GetMessageStatus.OFFSET_TOO_SMALL, getMessageResult.getStatus()); + Assert.assertEquals(100L, getMessageResult.getMinOffset()); + Assert.assertEquals(200L, getMessageResult.getMaxOffset()); + Assert.assertEquals(100L, getMessageResult.getNextBeginOffset()); + + getMessageResult = fetcher.getMessageFromTieredStoreAsync(flatFile, 200, 32).join(); + Assert.assertEquals(GetMessageStatus.OFFSET_OVERFLOW_ONE, getMessageResult.getStatus()); + Assert.assertEquals(100L, getMessageResult.getMinOffset()); + Assert.assertEquals(200L, getMessageResult.getMaxOffset()); + Assert.assertEquals(200L, getMessageResult.getNextBeginOffset()); + + getMessageResult = fetcher.getMessageFromTieredStoreAsync(flatFile, 300, 32).join(); + Assert.assertEquals(GetMessageStatus.OFFSET_OVERFLOW_BADLY, getMessageResult.getStatus()); + Assert.assertEquals(100L, getMessageResult.getMinOffset()); + Assert.assertEquals(200L, getMessageResult.getMaxOffset()); + Assert.assertEquals(200L, getMessageResult.getNextBeginOffset()); + + getMessageResult = fetcher.getMessageFromTieredStoreAsync(flatFile, 100, 32).join(); + Assert.assertEquals(GetMessageStatus.FOUND, getMessageResult.getStatus()); + Assert.assertEquals(100L, getMessageResult.getMinOffset()); + Assert.assertEquals(200L, getMessageResult.getMaxOffset()); + Assert.assertEquals(100L + 32L, getMessageResult.getNextBeginOffset()); + + getMessageResult = fetcher.getMessageFromTieredStoreAsync(flatFile, 180, 32).join(); + Assert.assertEquals(GetMessageStatus.FOUND, getMessageResult.getStatus()); + Assert.assertEquals(20, getMessageResult.getMessageCount()); + Assert.assertEquals(100L, getMessageResult.getMinOffset()); + Assert.assertEquals(200L, getMessageResult.getMaxOffset()); + Assert.assertEquals(200L, getMessageResult.getNextBeginOffset()); + + // limit count or size + int expect = 8; + int size = getMessageResult.getMessageBufferList().get(0).remaining(); + storeConfig.setReadAheadMessageSizeThreshold(expect * size + 10); + getMessageResult = fetcher.getMessageFromTieredStoreAsync(flatFile, 180, 32).join(); + Assert.assertEquals(GetMessageStatus.FOUND, getMessageResult.getStatus()); + Assert.assertEquals(expect, getMessageResult.getMessageCount()); + Assert.assertEquals(100L, getMessageResult.getMinOffset()); + Assert.assertEquals(200L, getMessageResult.getMaxOffset()); + Assert.assertEquals(180L + expect, getMessageResult.getNextBeginOffset()); + + storeConfig.setReadAheadMessageCountThreshold(expect); + storeConfig.setReadAheadMessageSizeThreshold(expect * size + expect * 2); + getMessageResult = fetcher.getMessageFromTieredStoreAsync(flatFile, 180, 32).join(); + Assert.assertEquals(GetMessageStatus.FOUND, getMessageResult.getStatus()); + Assert.assertEquals(expect, getMessageResult.getMessageCount()); + Assert.assertEquals(100L, getMessageResult.getMinOffset()); + Assert.assertEquals(200L, getMessageResult.getMaxOffset()); + Assert.assertEquals(180L + expect, getMessageResult.getNextBeginOffset()); + } + + @Test + public void getMessageFromCacheTest() throws Exception { + this.getMessageFromTieredStoreTest(); + mq = dispatcherTest.mq; + messageStore = dispatcherTest.messageStore; + storeConfig = dispatcherTest.storeConfig; + + storeConfig.setReadAheadCacheEnable(true); + storeConfig.setReadAheadMessageCountThreshold(32); + storeConfig.setReadAheadMessageSizeThreshold(Integer.MAX_VALUE); + + int batchSize = 4; + AtomicLong times = new AtomicLong(0L); + AtomicLong offset = new AtomicLong(100L); + FlatMessageFile flatFile = dispatcherTest.fileStore.getFlatFile(mq); + Awaitility.await().atMost(Duration.ofSeconds(10)).until(() -> { + GetMessageResult getMessageResult = + fetcher.getMessageFromCacheAsync(flatFile, groupName, offset.get(), batchSize, null).join(); + offset.set(getMessageResult.getNextBeginOffset()); + times.incrementAndGet(); + return offset.get() == 200L; + }); + Assert.assertEquals(100 / times.get(), batchSize); + } + + @Test + public void getMessageFromCacheTagFilterTest() throws Exception { + dispatcherTest.dispatchFromCommitLogTest(); + mq = dispatcherTest.mq; + messageStore = dispatcherTest.messageStore; + storeConfig = dispatcherTest.storeConfig; + + storeConfig.setReadAheadCacheEnable(true); + fetcher = new MessageStoreFetcherImpl(messageStore); + + FlatMessageFile flatFile = Mockito.mock(FlatMessageFile.class); + Mockito.when(flatFile.getMessageQueue()).thenReturn(mq); + Mockito.when(flatFile.getConsumeQueueMinOffset()).thenReturn(100L); + Mockito.when(flatFile.getConsumeQueueMaxOffset()).thenReturn(200L); + + for (int i = 100; i < 200; i++) { + ByteBuffer buffer = MessageFormatUtilTest.buildMockedMessageBuffer(); + SelectBufferResult bufferResult = new SelectBufferResult(buffer, i, buffer.remaining(), i % 2); + fetcher.getFetcherCache().put( + String.format(CACHE_KEY_FORMAT, mq.getTopic(), mq.getQueueId(), i), bufferResult); + } + + SubscriptionData subscriptionData = new SubscriptionData(); + subscriptionData.setSubString("1 || 2"); + subscriptionData.setCodeSet(Sets.newHashSet(1, 2)); + MessageFilter filter = new DefaultMessageFilter(subscriptionData); + + GetMessageResult getMessageResult = + fetcher.getMessageFromCacheAsync(flatFile, groupName, 100L, 32, filter).join(); + Assert.assertEquals(GetMessageStatus.FOUND, getMessageResult.getStatus()); + Assert.assertEquals(32, getMessageResult.getMessageCount()); + Assert.assertEquals(164L, getMessageResult.getNextBeginOffset()); + + getMessageResult = + fetcher.getMessageFromCacheAsync(flatFile, groupName, 164L, 32, filter).join(); + Assert.assertEquals(GetMessageStatus.FOUND, getMessageResult.getStatus()); + Assert.assertEquals(18, getMessageResult.getMessageCount()); + Assert.assertEquals(200L, getMessageResult.getNextBeginOffset()); + + getMessageResult = + fetcher.getMessageFromCacheAsync(flatFile, groupName, 200L, 32, filter).join(); + Assert.assertEquals(GetMessageStatus.NO_MATCHED_MESSAGE, getMessageResult.getStatus()); + Assert.assertEquals(200L, getMessageResult.getNextBeginOffset()); + + subscriptionData.setCodeSet(Sets.newHashSet(0)); + filter = new DefaultMessageFilter(subscriptionData); + getMessageResult = + fetcher.getMessageFromCacheAsync(flatFile, groupName, 100L, 32, filter).join(); + Assert.assertEquals(GetMessageStatus.FOUND, getMessageResult.getStatus()); + Assert.assertEquals(32, getMessageResult.getMessageCount()); + Assert.assertEquals(164L - 1L, getMessageResult.getNextBeginOffset()); + } + + @Test + public void getMessageFromCacheTagFilter2Test() throws Exception { + dispatcherTest.dispatchFromCommitLogTest(); + mq = dispatcherTest.mq; + messageStore = dispatcherTest.messageStore; + storeConfig = dispatcherTest.storeConfig; + + storeConfig.setReadAheadCacheEnable(true); + fetcher = new MessageStoreFetcherImpl(messageStore); + + FlatMessageFile flatFile = Mockito.mock(FlatMessageFile.class); + Mockito.when(flatFile.getMessageQueue()).thenReturn(mq); + Mockito.when(flatFile.getConsumeQueueMinOffset()).thenReturn(100L); + Mockito.when(flatFile.getConsumeQueueMaxOffset()).thenReturn(200L); + + for (int i = 100; i < 200; i++) { + ByteBuffer buffer = MessageFormatUtilTest.buildMockedMessageBuffer(); + SelectBufferResult bufferResult = new SelectBufferResult(buffer, i, buffer.remaining(), i - 100L); + fetcher.getFetcherCache().put( + String.format(CACHE_KEY_FORMAT, mq.getTopic(), mq.getQueueId(), i), bufferResult); + } + + SubscriptionData subscriptionData = new SubscriptionData(); + subscriptionData.setSubString("1 || 2"); + subscriptionData.setCodeSet(Sets.newHashSet(10, 20)); + MessageFilter filter = new DefaultMessageFilter(subscriptionData); + + GetMessageResult getMessageResult = + fetcher.getMessageFromCacheAsync(flatFile, groupName, 100L, 2, filter).join(); + Assert.assertEquals(GetMessageStatus.FOUND, getMessageResult.getStatus()); + Assert.assertEquals(2, getMessageResult.getMessageCount()); + Assert.assertEquals(121L, getMessageResult.getNextBeginOffset()); + } + + @Test + public void testGetMessageStoreTimeStampAsync() throws Exception { + this.getMessageFromTieredStoreTest(); + mq = dispatcherTest.mq; + messageStore = dispatcherTest.messageStore; + storeConfig = dispatcherTest.storeConfig; + + long result1 = fetcher.getEarliestMessageTimeAsync(mq.getTopic(), 0).join(); + Assert.assertEquals(-1L, result1); + + long result2 = fetcher.getEarliestMessageTimeAsync(mq.getTopic(), mq.getQueueId()).join(); + Assert.assertEquals(11L, result2); + + long result3 = fetcher.getMessageStoreTimeStampAsync(mq.getTopic(), 0, 100).join(); + Assert.assertEquals(-1L, result3); + + long result4 = fetcher.getMessageStoreTimeStampAsync(mq.getTopic(), mq.getQueueId(), 100).join(); + Assert.assertEquals(11L, result4); + + long result5 = fetcher.getMessageStoreTimeStampAsync(mq.getTopic(), mq.getQueueId(), 120).join(); + Assert.assertEquals(11L, result5); + } + + @Test + public void testGetOffsetInQueueByTime() throws Exception { + this.getMessageFromTieredStoreTest(); + mq = dispatcherTest.mq; + messageStore = dispatcherTest.messageStore; + storeConfig = dispatcherTest.storeConfig; + + // message time is all 11 + Assert.assertEquals(-1L, fetcher.getOffsetInQueueByTime(mq.getTopic(), 0, 10, BoundaryType.LOWER)); + + Assert.assertEquals(100L, fetcher.getOffsetInQueueByTime(mq.getTopic(), 1, 10, BoundaryType.LOWER)); + Assert.assertEquals(100L, fetcher.getOffsetInQueueByTime(mq.getTopic(), 1, 11, BoundaryType.LOWER)); + Assert.assertEquals(200L, fetcher.getOffsetInQueueByTime(mq.getTopic(), 1, 12, BoundaryType.LOWER)); + + Assert.assertEquals(100L, fetcher.getOffsetInQueueByTime(mq.getTopic(), 1, 10, BoundaryType.UPPER)); + Assert.assertEquals(199L, fetcher.getOffsetInQueueByTime(mq.getTopic(), 1, 11, BoundaryType.UPPER)); + Assert.assertEquals(200L, fetcher.getOffsetInQueueByTime(mq.getTopic(), 1, 12, BoundaryType.UPPER)); + } + + @Test + public void testQueryMessageAsync() throws Exception { + this.getMessageFromTieredStoreTest(); + mq = dispatcherTest.mq; + messageStore = dispatcherTest.messageStore; + storeConfig = dispatcherTest.storeConfig; + + QueryMessageResult queryMessageResult = fetcher.queryMessageAsync( + mq.getTopic(), "uk", 32, 0L, System.currentTimeMillis()).join(); + Assert.assertEquals(32, queryMessageResult.getMessageBufferList().size()); + + queryMessageResult = fetcher.queryMessageAsync( + mq.getTopic(), "uk", 120, 0L, System.currentTimeMillis()).join(); + Assert.assertEquals(100, queryMessageResult.getMessageBufferList().size()); + } +} diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/core/MessageStoreTopicFilterTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/core/MessageStoreTopicFilterTest.java new file mode 100644 index 0000000..d5c3703 --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/core/MessageStoreTopicFilterTest.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.tieredstore.core; + +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.junit.Assert; +import org.junit.Test; + +public class MessageStoreTopicFilterTest { + + @Test + public void filterTopicTest() { + MessageStoreFilter topicFilter = new MessageStoreTopicFilter(new MessageStoreConfig()); + Assert.assertTrue(topicFilter.filterTopic("")); + Assert.assertTrue(topicFilter.filterTopic(TopicValidator.SYSTEM_TOPIC_PREFIX + "_Topic")); + + String topicName = "WhiteTopic"; + Assert.assertFalse(topicFilter.filterTopic(topicName)); + topicFilter.addTopicToBlackList(topicName); + Assert.assertTrue(topicFilter.filterTopic(topicName)); + } +} \ No newline at end of file diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/exception/TieredStoreExceptionTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/exception/TieredStoreExceptionTest.java new file mode 100644 index 0000000..1de891a --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/exception/TieredStoreExceptionTest.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.tieredstore.exception; + +import org.junit.Assert; +import org.junit.Test; + +public class TieredStoreExceptionTest { + + @Test + public void testMessageStoreException() { + long position = 100L; + String requestId = "requestId"; + String error = "ErrorMessage"; + + TieredStoreException tieredStoreException = new TieredStoreException(TieredStoreErrorCode.IO_ERROR, error); + Assert.assertEquals(TieredStoreErrorCode.IO_ERROR, tieredStoreException.getErrorCode()); + Assert.assertEquals(error, tieredStoreException.getMessage()); + + tieredStoreException.setRequestId(requestId); + Assert.assertEquals(requestId, tieredStoreException.getRequestId()); + + tieredStoreException.setPosition(position); + Assert.assertEquals(position, tieredStoreException.getPosition()); + Assert.assertNotNull(tieredStoreException.toString()); + } +} \ No newline at end of file diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/file/FlatAppendFileTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/file/FlatAppendFileTest.java new file mode 100644 index 0000000..2e69437 --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/file/FlatAppendFileTest.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.tieredstore.file; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.concurrent.CompletionException; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.common.FileSegmentType; +import org.apache.rocketmq.tieredstore.exception.TieredStoreErrorCode; +import org.apache.rocketmq.tieredstore.exception.TieredStoreException; +import org.apache.rocketmq.tieredstore.metadata.DefaultMetadataStore; +import org.apache.rocketmq.tieredstore.metadata.MetadataStore; +import org.apache.rocketmq.tieredstore.metadata.entity.FileSegmentMetadata; +import org.apache.rocketmq.tieredstore.provider.FileSegment; +import org.apache.rocketmq.tieredstore.provider.PosixFileSegment; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtil; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtilTest; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class FlatAppendFileTest { + + private final String storePath = MessageStoreUtilTest.getRandomStorePath(); + private MessageQueue queue; + private MetadataStore metadataStore; + private MessageStoreConfig storeConfig; + private FlatFileFactory flatFileFactory; + + @Before + public void init() throws ClassNotFoundException, NoSuchMethodException { + storeConfig = new MessageStoreConfig(); + storeConfig.setBrokerName("brokerName"); + storeConfig.setStorePathRootDir(storePath); + storeConfig.setTieredStoreFilePath(storePath); + storeConfig.setTieredBackendServiceProvider(PosixFileSegment.class.getName()); + storeConfig.setTieredStoreCommitLogMaxSize(2000L); + storeConfig.setTieredStoreConsumeQueueMaxSize(2000L); + queue = new MessageQueue("TieredFlatFileTest", storeConfig.getBrokerName(), 0); + metadataStore = new DefaultMetadataStore(storeConfig); + flatFileFactory = new FlatFileFactory(metadataStore, storeConfig); + } + + @After + public void shutdown() throws IOException { + MessageStoreUtilTest.deleteStoreDirectory(storePath); + } + + public ByteBuffer allocateBuffer(int size) { + byte[] byteArray = new byte[size]; + ByteBuffer buffer = ByteBuffer.wrap(byteArray); + Arrays.fill(byteArray, (byte) 0); + return buffer; + } + + @Test + public void recoverFileSizeTest() { + String filePath = MessageStoreUtil.toFilePath(queue); + FlatAppendFile flatFile = flatFileFactory.createFlatFileForConsumeQueue(filePath); + flatFile.rollingNewFile(500L); + + FileSegment fileSegment = flatFile.getFileToWrite(); + flatFile.append(allocateBuffer(1000), 1L); + flatFile.commitAsync().join(); + flatFile.flushFileSegmentMeta(fileSegment); + } + + @Test + public void testRecoverFile() { + String filePath = MessageStoreUtil.toFilePath(queue); + FlatAppendFile flatFile = flatFileFactory.createFlatFileForConsumeQueue(filePath); + flatFile.rollingNewFile(500L); + + FileSegment fileSegment = flatFile.getFileToWrite(); + flatFile.append(allocateBuffer(1000), 1L); + flatFile.commitAsync().join(); + flatFile.flushFileSegmentMeta(fileSegment); + + FileSegmentMetadata metadata = + metadataStore.getFileSegment(filePath, FileSegmentType.CONSUME_QUEUE, 500L); + Assert.assertEquals(fileSegment.getPath(), metadata.getPath()); + Assert.assertEquals(FileSegmentType.CONSUME_QUEUE, FileSegmentType.valueOf(metadata.getType())); + Assert.assertEquals(500L, metadata.getBaseOffset()); + Assert.assertEquals(1000L, metadata.getSize()); + Assert.assertEquals(0L, metadata.getSealTimestamp()); + + fileSegment.close(); + flatFile.rollingNewFile(flatFile.getAppendOffset()); + flatFile.append(allocateBuffer(200), 1L); + flatFile.commitAsync().join(); + flatFile.flushFileSegmentMeta(fileSegment); + Assert.assertEquals(2, flatFile.getFileSegmentList().size()); + flatFile.getFileToWrite().close(); + + metadata = metadataStore.getFileSegment(filePath, FileSegmentType.CONSUME_QUEUE, 1500L); + Assert.assertEquals(fileSegment.getPath(), metadata.getPath()); + Assert.assertEquals(FileSegmentType.CONSUME_QUEUE, FileSegmentType.valueOf(metadata.getType())); + Assert.assertEquals(1500L, metadata.getBaseOffset()); + Assert.assertEquals(200L, metadata.getSize()); + Assert.assertEquals(0L, metadata.getSealTimestamp()); + + // reference same file + flatFile = flatFileFactory.createFlatFileForConsumeQueue(filePath); + Assert.assertEquals(2, flatFile.fileSegmentTable.size()); + metadata = metadataStore.getFileSegment(filePath, FileSegmentType.CONSUME_QUEUE, 1500L); + Assert.assertEquals(fileSegment.getPath(), metadata.getPath()); + Assert.assertEquals(FileSegmentType.CONSUME_QUEUE, FileSegmentType.valueOf(metadata.getType())); + Assert.assertEquals(1500L, metadata.getBaseOffset()); + Assert.assertEquals(200L, metadata.getSize()); + Assert.assertEquals(0L, metadata.getSealTimestamp()); + flatFile.destroy(); + } + + @Test + public void testFileSegment() { + String filePath = MessageStoreUtil.toFilePath(queue); + FlatAppendFile flatFile = flatFileFactory.createFlatFileForConsumeQueue(filePath); + Assert.assertThrows(IllegalStateException.class, flatFile::getFileToWrite); + + flatFile.commitAsync().join(); + flatFile.rollingNewFile(0L); + Assert.assertEquals(0L, flatFile.getMinOffset()); + Assert.assertEquals(0L, flatFile.getCommitOffset()); + Assert.assertEquals(0L, flatFile.getAppendOffset()); + + flatFile.append(allocateBuffer(1000), 1L); + Assert.assertEquals(0L, flatFile.getMinOffset()); + Assert.assertEquals(0L, flatFile.getCommitOffset()); + Assert.assertEquals(1000L, flatFile.getAppendOffset()); + Assert.assertEquals(1L, flatFile.getMinTimestamp()); + Assert.assertEquals(1L, flatFile.getMaxTimestamp()); + + flatFile.commitAsync().join(); + Assert.assertEquals(filePath, flatFile.getFilePath()); + Assert.assertEquals(FileSegmentType.CONSUME_QUEUE, flatFile.getFileType()); + Assert.assertEquals(0L, flatFile.getMinOffset()); + Assert.assertEquals(1000L, flatFile.getCommitOffset()); + Assert.assertEquals(1000L, flatFile.getAppendOffset()); + Assert.assertEquals(1L, flatFile.getMinTimestamp()); + Assert.assertEquals(1L, flatFile.getMaxTimestamp()); + + // file full + flatFile.append(allocateBuffer(1000), 1L); + flatFile.append(allocateBuffer(1000), 1L); + flatFile.commitAsync().join(); + Assert.assertEquals(2, flatFile.fileSegmentTable.size()); + flatFile.destroy(); + } + + @Test + public void testAppendAndRead() { + FlatAppendFile flatFile = flatFileFactory.createFlatFileForConsumeQueue(MessageStoreUtil.toFilePath(queue)); + flatFile.rollingNewFile(500L); + Assert.assertEquals(500L, flatFile.getCommitOffset()); + Assert.assertEquals(500L, flatFile.getAppendOffset()); + + flatFile.append(allocateBuffer(1000), 1L); + + // no commit + CompletionException exception = Assert.assertThrows( + CompletionException.class, () -> flatFile.readAsync(500, 200).join()); + Assert.assertTrue(exception.getCause() instanceof TieredStoreException); + Assert.assertEquals(TieredStoreErrorCode.ILLEGAL_PARAM, + ((TieredStoreException) exception.getCause()).getErrorCode()); + flatFile.commitAsync().join(); + Assert.assertEquals(200, flatFile.readAsync(500, 200).join().remaining()); + + // 500-1500, 1500-3000 + flatFile.append(allocateBuffer(1500), 1L); + flatFile.commitAsync().join(); + Assert.assertEquals(2, flatFile.fileSegmentTable.size()); + Assert.assertEquals(1000, flatFile.readAsync(1000, 1000).join().remaining()); + flatFile.destroy(); + } + + @Test + public void testCleanExpiredFile() { + FlatAppendFile flatFile = flatFileFactory.createFlatFileForConsumeQueue(MessageStoreUtil.toFilePath(queue)); + flatFile.destroyExpiredFile(1); + + flatFile.rollingNewFile(500L); + flatFile.append(allocateBuffer(1000), 2L); + flatFile.commitAsync().join(); + Assert.assertEquals(1, flatFile.fileSegmentTable.size()); + flatFile.destroyExpiredFile(1); + Assert.assertEquals(1, flatFile.fileSegmentTable.size()); + flatFile.destroyExpiredFile(3); + Assert.assertEquals(0, flatFile.fileSegmentTable.size()); + + flatFile.rollingNewFile(1500L); + flatFile.append(allocateBuffer(1000), 2L); + flatFile.append(allocateBuffer(1000), 2L); + flatFile.commitAsync().join(); + flatFile.destroy(); + Assert.assertEquals(0, flatFile.fileSegmentTable.size()); + } +} diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/file/FlatCommitLogFileTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/file/FlatCommitLogFileTest.java new file mode 100644 index 0000000..0fbf5a6 --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/file/FlatCommitLogFileTest.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.tieredstore.file; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.common.AppendResult; +import org.apache.rocketmq.tieredstore.metadata.DefaultMetadataStore; +import org.apache.rocketmq.tieredstore.metadata.MetadataStore; +import org.apache.rocketmq.tieredstore.provider.PosixFileSegment; +import org.apache.rocketmq.tieredstore.util.MessageFormatUtil; +import org.apache.rocketmq.tieredstore.util.MessageFormatUtilTest; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtil; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtilTest; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class FlatCommitLogFileTest { + + private final String storePath = MessageStoreUtilTest.getRandomStorePath(); + private MessageQueue queue; + private MetadataStore metadataStore; + private MessageStoreConfig storeConfig; + private FlatFileFactory flatFileFactory; + + @Before + public void init() throws ClassNotFoundException, NoSuchMethodException { + storeConfig = new MessageStoreConfig(); + storeConfig.setBrokerName("brokerName"); + storeConfig.setStorePathRootDir(storePath); + storeConfig.setTieredStoreFilePath(storePath); + storeConfig.setTieredBackendServiceProvider(PosixFileSegment.class.getName()); + storeConfig.setTieredStoreCommitLogMaxSize(2000L); + storeConfig.setTieredStoreConsumeQueueMaxSize(2000L); + queue = new MessageQueue("TieredFlatFileTest", storeConfig.getBrokerName(), 0); + metadataStore = new DefaultMetadataStore(storeConfig); + flatFileFactory = new FlatFileFactory(metadataStore, storeConfig); + } + + @After + public void shutdown() throws IOException { + MessageStoreUtilTest.deleteStoreDirectory(storePath); + } + + @Test + public void constructTest() { + String filePath = MessageStoreUtil.toFilePath(queue); + FlatAppendFile flatFile = flatFileFactory.createFlatFileForCommitLog(filePath); + Assert.assertEquals(1L, flatFile.fileSegmentTable.size()); + } + + @Test + public void tryRollingFileTest() throws InterruptedException { + String filePath = MessageStoreUtil.toFilePath(queue); + FlatCommitLogFile flatFile = flatFileFactory.createFlatFileForCommitLog(filePath); + for (int i = 0; i < 3; i++) { + ByteBuffer byteBuffer = MessageFormatUtilTest.buildMockedMessageBuffer(); + byteBuffer.putLong(MessageFormatUtil.QUEUE_OFFSET_POSITION, i); + Assert.assertEquals(AppendResult.SUCCESS, flatFile.append(byteBuffer, i)); + TimeUnit.MILLISECONDS.sleep(2); + storeConfig.setCommitLogRollingMinimumSize(byteBuffer.remaining()); + Assert.assertTrue(flatFile.tryRollingFile(1)); + } + Assert.assertEquals(4, flatFile.fileSegmentTable.size()); + Assert.assertFalse(flatFile.tryRollingFile(1000)); + flatFile.destroy(); + } + + @Test + public void getMinOffsetFromFileAsyncTest() { + String filePath = MessageStoreUtil.toFilePath(queue); + FlatCommitLogFile flatFile = flatFileFactory.createFlatFileForCommitLog(filePath); + + // append some messages + for (int i = 6; i < 9; i++) { + ByteBuffer byteBuffer = MessageFormatUtilTest.buildMockedMessageBuffer(); + byteBuffer.putLong(MessageFormatUtil.QUEUE_OFFSET_POSITION, i); + Assert.assertEquals(AppendResult.SUCCESS, flatFile.append(byteBuffer, i)); + } + Assert.assertEquals(-1L, flatFile.getMinOffsetFromFileAsync().join().longValue()); + + // append some messages + for (int i = 9; i < 30; i++) { + if (i == 20) { + flatFile.commitAsync().join(); + flatFile.rollingNewFile(flatFile.getAppendOffset()); + } + ByteBuffer byteBuffer = MessageFormatUtilTest.buildMockedMessageBuffer(); + byteBuffer.putLong(MessageFormatUtil.QUEUE_OFFSET_POSITION, i); + Assert.assertEquals(AppendResult.SUCCESS, flatFile.append(byteBuffer, i)); + } + + flatFile.commitAsync().join(); + Assert.assertEquals(6L, flatFile.getMinOffsetFromFile()); + Assert.assertEquals(6L, flatFile.getMinOffsetFromFileAsync().join().longValue()); + + // recalculate min offset here + flatFile.destroyExpiredFile(20L); + Assert.assertEquals(20L, flatFile.getMinOffsetFromFile()); + Assert.assertEquals(20L, flatFile.getMinOffsetFromFileAsync().join().longValue()); + + // clean expired file again + flatFile.destroyExpiredFile(20L); + Assert.assertEquals(20L, flatFile.getMinOffsetFromFile()); + Assert.assertEquals(20L, flatFile.getMinOffsetFromFileAsync().join().longValue()); + } +} \ No newline at end of file diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/file/FlatFileFactoryTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/file/FlatFileFactoryTest.java new file mode 100644 index 0000000..bc8ebaf --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/file/FlatFileFactoryTest.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.tieredstore.file; + +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.metadata.DefaultMetadataStore; +import org.apache.rocketmq.tieredstore.metadata.MetadataStore; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtilTest; +import org.junit.Assert; +import org.junit.Test; + +public class FlatFileFactoryTest { + + @Test + public void factoryTest() { + MessageStoreConfig storeConfig = new MessageStoreConfig(); + storeConfig.setTieredStoreFilePath(MessageStoreUtilTest.getRandomStorePath()); + MetadataStore metadataStore = new DefaultMetadataStore(storeConfig); + FlatFileFactory factory = new FlatFileFactory(metadataStore, storeConfig); + Assert.assertEquals(storeConfig, factory.getStoreConfig()); + Assert.assertEquals(metadataStore, factory.getMetadataStore()); + + FlatAppendFile flatFile1 = factory.createFlatFileForCommitLog("CommitLog"); + FlatAppendFile flatFile2 = factory.createFlatFileForConsumeQueue("ConsumeQueue"); + FlatAppendFile flatFile3 = factory.createFlatFileForIndexFile("IndexFile"); + + Assert.assertNotNull(flatFile1); + Assert.assertNotNull(flatFile2); + Assert.assertNotNull(flatFile3); + + flatFile1.destroy(); + flatFile2.destroy(); + flatFile3.destroy(); + } +} \ No newline at end of file diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/file/FlatFileStoreTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/file/FlatFileStoreTest.java new file mode 100644 index 0000000..2a007af --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/file/FlatFileStoreTest.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.tieredstore.file; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.MessageStoreExecutor; +import org.apache.rocketmq.tieredstore.exception.TieredStoreErrorCode; +import org.apache.rocketmq.tieredstore.exception.TieredStoreException; +import org.apache.rocketmq.tieredstore.metadata.DefaultMetadataStore; +import org.apache.rocketmq.tieredstore.metadata.MetadataStore; +import org.apache.rocketmq.tieredstore.provider.PosixFileSegment; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtilTest; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import static org.mockito.ArgumentMatchers.any; + +public class FlatFileStoreTest { + + private final String storePath = MessageStoreUtilTest.getRandomStorePath(); + private MessageStoreConfig storeConfig; + private MetadataStore metadataStore; + + @Before + public void init() { + storeConfig = new MessageStoreConfig(); + storeConfig.setStorePathRootDir(storePath); + storeConfig.setTieredBackendServiceProvider(PosixFileSegment.class.getName()); + storeConfig.setBrokerName("brokerName"); + metadataStore = new DefaultMetadataStore(storeConfig); + } + + @After + public void shutdown() throws IOException { + MessageStoreUtilTest.deleteStoreDirectory(storePath); + } + + @Test + public void flatFileStoreTest() { + // Empty recover + MessageStoreExecutor executor = new MessageStoreExecutor(); + FlatFileStore fileStore = new FlatFileStore(storeConfig, metadataStore, executor); + Assert.assertTrue(fileStore.load()); + + Assert.assertEquals(storeConfig, fileStore.getStoreConfig()); + Assert.assertEquals(metadataStore, fileStore.getMetadataStore()); + Assert.assertNotNull(fileStore.getFlatFileFactory()); + + for (int i = 0; i < 4; i++) { + MessageQueue mq = new MessageQueue("flatFileStoreTest", storeConfig.getBrokerName(), i); + FlatMessageFile flatFile = fileStore.computeIfAbsent(mq); + FlatMessageFile flatFileGet = fileStore.getFlatFile(mq); + Assert.assertEquals(flatFile, flatFileGet); + } + Assert.assertEquals(4, fileStore.deepCopyFlatFileToList().size()); + fileStore.shutdown(); + + fileStore = new FlatFileStore(storeConfig, metadataStore, executor); + Assert.assertTrue(fileStore.load()); + Assert.assertEquals(4, fileStore.deepCopyFlatFileToList().size()); + + for (int i = 1; i < 3; i++) { + MessageQueue mq = new MessageQueue("flatFileStoreTest", storeConfig.getBrokerName(), i); + fileStore.destroyFile(mq); + } + Assert.assertEquals(2, fileStore.deepCopyFlatFileToList().size()); + fileStore.shutdown(); + + FlatFileStore fileStoreSpy = Mockito.spy(fileStore); + Mockito.when(fileStoreSpy.recoverAsync(any())).thenReturn(CompletableFuture.supplyAsync(() -> { + throw new TieredStoreException(TieredStoreErrorCode.ILLEGAL_PARAM, "Test"); + })); + Assert.assertFalse(fileStoreSpy.load()); + + Mockito.reset(fileStoreSpy); + fileStore.load(); + Assert.assertEquals(2, fileStore.deepCopyFlatFileToList().size()); + fileStore.destroy(); + Assert.assertEquals(0, fileStore.deepCopyFlatFileToList().size()); + } +} diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/file/FlatMessageFileTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/file/FlatMessageFileTest.java new file mode 100644 index 0000000..97768d0 --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/file/FlatMessageFileTest.java @@ -0,0 +1,249 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tieredstore.file; + +import java.io.IOException; +import java.nio.ByteBuffer; +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.store.ConsumeQueue; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.common.AppendResult; +import org.apache.rocketmq.tieredstore.metadata.DefaultMetadataStore; +import org.apache.rocketmq.tieredstore.metadata.MetadataStore; +import org.apache.rocketmq.tieredstore.provider.PosixFileSegment; +import org.apache.rocketmq.tieredstore.util.MessageFormatUtil; +import org.apache.rocketmq.tieredstore.util.MessageFormatUtilTest; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtil; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtilTest; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class FlatMessageFileTest { + + private final String storePath = MessageStoreUtilTest.getRandomStorePath(); + private MessageStoreConfig storeConfig; + private MetadataStore metadataStore; + private FlatFileFactory flatFileFactory; + + @Before + public void init() throws ClassNotFoundException, NoSuchMethodException { + storeConfig = new MessageStoreConfig(); + storeConfig.setBrokerName("brokerName"); + storeConfig.setStorePathRootDir(storePath); + storeConfig.setTieredBackendServiceProvider(PosixFileSegment.class.getName()); + storeConfig.setCommitLogRollingInterval(0); + storeConfig.setCommitLogRollingMinimumSize(999); + metadataStore = new DefaultMetadataStore(storeConfig); + flatFileFactory = new FlatFileFactory(metadataStore, storeConfig); + } + + @After + public void shutdown() throws IOException { + MessageStoreUtilTest.deleteStoreDirectory(storePath); + } + + @Test + public void testAppendCommitLog() { + String topic = "CommitLogTest"; + FlatMessageFile flatFile = new FlatMessageFile(flatFileFactory, topic, 0); + Assert.assertTrue(flatFile.getTopicId() >= 0); + Assert.assertEquals(topic, flatFile.getMessageQueue().getTopic()); + Assert.assertEquals(0, flatFile.getMessageQueue().getQueueId()); + Assert.assertFalse(flatFile.isFlatFileInit()); + + flatFile.flushMetadata(); + Assert.assertNotNull(metadataStore.getQueue(flatFile.getMessageQueue())); + + long offset = 100; + flatFile.initOffset(offset); + for (int i = 0; i < 5; i++) { + ByteBuffer buffer = MessageFormatUtilTest.buildMockedMessageBuffer(); + DispatchRequest request = new DispatchRequest( + topic, 0, i, (long) buffer.remaining() * i, buffer.remaining(), 0L); + flatFile.appendCommitLog(buffer); + flatFile.appendConsumeQueue(request); + } + + Assert.assertNotNull(flatFile.getFileLock()); + + long time = MessageFormatUtil.getStoreTimeStamp(MessageFormatUtilTest.buildMockedMessageBuffer()); + Assert.assertEquals(time, flatFile.getMinStoreTimestamp()); + Assert.assertEquals(time, flatFile.getMaxStoreTimestamp()); + + long size = MessageFormatUtilTest.buildMockedMessageBuffer().remaining(); + Assert.assertEquals(-1L, flatFile.getFirstMessageOffset()); + Assert.assertEquals(0L, flatFile.getCommitLogMinOffset()); + Assert.assertEquals(0L, flatFile.getCommitLogCommitOffset()); + Assert.assertEquals(5 * size, flatFile.getCommitLogMaxOffset()); + + Assert.assertEquals(offset, flatFile.getConsumeQueueMinOffset()); + Assert.assertEquals(offset, flatFile.getConsumeQueueCommitOffset()); + Assert.assertEquals(offset + 5L, flatFile.getConsumeQueueMaxOffset()); + + Assert.assertTrue(flatFile.commitAsync().join()); + Assert.assertEquals(6L, flatFile.getFirstMessageOffset()); + Assert.assertEquals(0L, flatFile.getCommitLogMinOffset()); + Assert.assertEquals(5 * size, flatFile.getCommitLogCommitOffset()); + Assert.assertEquals(5 * size, flatFile.getCommitLogMaxOffset()); + + Assert.assertEquals(offset, flatFile.getConsumeQueueMinOffset()); + Assert.assertEquals(offset + 5L, flatFile.getConsumeQueueCommitOffset()); + Assert.assertEquals(offset + 5L, flatFile.getConsumeQueueMaxOffset()); + + // test read + ByteBuffer buffer = flatFile.getMessageAsync(offset).join(); + Assert.assertNotNull(buffer); + Assert.assertEquals(size, buffer.remaining()); + Assert.assertEquals(6L, MessageFormatUtil.getQueueOffset(buffer)); + + flatFile.destroyExpiredFile(0); + flatFile.destroy(); + } + + @Test + public void testEquals() { + String topic = "EqualsTest"; + FlatMessageFile flatFile1 = new FlatMessageFile(flatFileFactory, topic, 0); + FlatMessageFile flatFile2 = new FlatMessageFile(flatFileFactory, topic, 0); + FlatMessageFile flatFile3 = new FlatMessageFile(flatFileFactory, topic, 1); + Assert.assertEquals(flatFile1, flatFile2); + Assert.assertEquals(flatFile1.hashCode(), flatFile2.hashCode()); + Assert.assertNotEquals(flatFile1, flatFile3); + + flatFile1.shutdown(); + flatFile2.shutdown(); + flatFile3.shutdown(); + + flatFile1.destroy(); + flatFile2.destroy(); + flatFile3.destroy(); + } + + @Test + public void testBinarySearchInQueueByTime() { + + // replace provider, need new factory again + storeConfig.setTieredBackendServiceProvider(PosixFileSegment.class.getName()); + flatFileFactory = new FlatFileFactory(metadataStore, storeConfig); + + // inject store time: 0, +100, +100, +100, +200 + MessageQueue mq = new MessageQueue("TopicTest", "BrokerName", 1); + FlatMessageFile flatFile = new FlatMessageFile(flatFileFactory, MessageStoreUtil.toFilePath(mq)); + flatFile.initOffset(50); + long timestamp1 = 1000; + ByteBuffer buffer = MessageFormatUtilTest.buildMockedMessageBuffer(); + buffer.putLong(MessageFormatUtil.QUEUE_OFFSET_POSITION, 50); + buffer.putLong(MessageFormatUtil.STORE_TIMESTAMP_POSITION, timestamp1); + flatFile.appendCommitLog(buffer); + + long timestamp2 = timestamp1 + 100; + buffer = MessageFormatUtilTest.buildMockedMessageBuffer(); + buffer.putLong(MessageFormatUtil.QUEUE_OFFSET_POSITION, 51); + buffer.putLong(MessageFormatUtil.STORE_TIMESTAMP_POSITION, timestamp2); + flatFile.appendCommitLog(buffer); + buffer = MessageFormatUtilTest.buildMockedMessageBuffer(); + buffer.putLong(MessageFormatUtil.QUEUE_OFFSET_POSITION, 52); + buffer.putLong(MessageFormatUtil.STORE_TIMESTAMP_POSITION, timestamp2); + flatFile.appendCommitLog(buffer); + buffer = MessageFormatUtilTest.buildMockedMessageBuffer(); + buffer.putLong(MessageFormatUtil.QUEUE_OFFSET_POSITION, 53); + buffer.putLong(MessageFormatUtil.STORE_TIMESTAMP_POSITION, timestamp2); + flatFile.appendCommitLog(buffer); + + long timestamp3 = timestamp2 + 100; + buffer = MessageFormatUtilTest.buildMockedMessageBuffer(); + buffer.putLong(MessageFormatUtil.QUEUE_OFFSET_POSITION, 54); + buffer.putLong(MessageFormatUtil.STORE_TIMESTAMP_POSITION, timestamp3); + flatFile.appendCommitLog(buffer); + + // append message to consume queue + flatFile.consumeQueue.initOffset(50 * ConsumeQueue.CQ_STORE_UNIT_SIZE); + + AppendResult appendResult = flatFile.appendConsumeQueue(new DispatchRequest( + mq.getTopic(), mq.getQueueId(), 0, + MessageFormatUtilTest.MSG_LEN, 0, timestamp1, 50, + "", "", 0, 0, null)); + Assert.assertEquals(AppendResult.SUCCESS, appendResult); + + appendResult = flatFile.appendConsumeQueue(new DispatchRequest( + mq.getTopic(), mq.getQueueId(), MessageFormatUtilTest.MSG_LEN, + MessageFormatUtilTest.MSG_LEN, 0, timestamp2, 51, + "", "", 0, 0, null)); + Assert.assertEquals(AppendResult.SUCCESS, appendResult); + + appendResult = flatFile.appendConsumeQueue(new DispatchRequest( + mq.getTopic(), mq.getQueueId(), MessageFormatUtilTest.MSG_LEN * 2, + MessageFormatUtilTest.MSG_LEN, 0, timestamp2, 52, + "", "", 0, 0, null)); + Assert.assertEquals(AppendResult.SUCCESS, appendResult); + + appendResult = flatFile.appendConsumeQueue(new DispatchRequest( + mq.getTopic(), mq.getQueueId(), MessageFormatUtilTest.MSG_LEN * 3, + MessageFormatUtilTest.MSG_LEN, 0, timestamp2, 53, + "", "", 0, 0, null)); + Assert.assertEquals(AppendResult.SUCCESS, appendResult); + + appendResult = flatFile.appendConsumeQueue(new DispatchRequest( + mq.getTopic(), mq.getQueueId(), MessageFormatUtilTest.MSG_LEN * 4, + MessageFormatUtilTest.MSG_LEN, 0, timestamp3, 54, + "", "", 0, 0, null)); + Assert.assertEquals(AppendResult.SUCCESS, appendResult); + + // commit message will increase max consume queue offset + Assert.assertTrue(flatFile.commitAsync().join()); + + // offset: 50, 51, 52, 53, 54 + // inject store time: 0, +100, +100, +100, +200 + Assert.assertEquals(50, flatFile.getQueueOffsetByTimeAsync(0, BoundaryType.LOWER).join().longValue()); + Assert.assertEquals(50, flatFile.getQueueOffsetByTimeAsync(0, BoundaryType.UPPER).join().longValue()); + + Assert.assertEquals(50, flatFile.getQueueOffsetByTimeAsync(timestamp1 - 1, BoundaryType.LOWER).join().longValue()); + Assert.assertEquals(50, flatFile.getQueueOffsetByTimeAsync(timestamp1 - 1, BoundaryType.UPPER).join().longValue()); + + Assert.assertEquals(50, flatFile.getQueueOffsetByTimeAsync(timestamp1, BoundaryType.LOWER).join().longValue()); + Assert.assertEquals(50, flatFile.getQueueOffsetByTimeAsync(timestamp1, BoundaryType.UPPER).join().longValue()); + + Assert.assertEquals(51, flatFile.getQueueOffsetByTimeAsync(timestamp1 + 1, BoundaryType.LOWER).join().longValue()); + Assert.assertEquals(51, flatFile.getQueueOffsetByTimeAsync(timestamp1 + 1, BoundaryType.UPPER).join().longValue()); + + Assert.assertEquals(51, flatFile.getQueueOffsetByTimeAsync(timestamp2, BoundaryType.LOWER).join().longValue()); + Assert.assertEquals(53, flatFile.getQueueOffsetByTimeAsync(timestamp2, BoundaryType.UPPER).join().longValue()); + + Assert.assertEquals(54, flatFile.getQueueOffsetByTimeAsync(timestamp2 + 1, BoundaryType.UPPER).join().longValue()); + Assert.assertEquals(54, flatFile.getQueueOffsetByTimeAsync(timestamp2 + 1, BoundaryType.LOWER).join().longValue()); + + Assert.assertEquals(54, flatFile.getQueueOffsetByTimeAsync(timestamp3, BoundaryType.LOWER).join().longValue()); + Assert.assertEquals(54, flatFile.getQueueOffsetByTimeAsync(timestamp3, BoundaryType.UPPER).join().longValue()); + + Assert.assertEquals(55, flatFile.getQueueOffsetByTimeAsync(timestamp3 + 1, BoundaryType.LOWER).join().longValue()); + Assert.assertEquals(55, flatFile.getQueueOffsetByTimeAsync(timestamp3 + 1, BoundaryType.UPPER).join().longValue()); + + flatFile.destroy(); + } + + @Test + public void testCommitLock() { + String topic = "CommitLogTest"; + FlatMessageFile flatFile = new FlatMessageFile(flatFileFactory, topic, 0); + flatFile.getCommitLock().drainPermits(); + Assert.assertFalse(flatFile.commitAsync().join()); + } +} diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/index/IndexItemTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/index/IndexItemTest.java new file mode 100644 index 0000000..22ed4cc --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/index/IndexItemTest.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.tieredstore.index; + +import java.nio.ByteBuffer; +import org.junit.Assert; +import org.junit.Test; + +public class IndexItemTest { + + private final int topicId = 1; + private final int queueId = 2; + private final long offset = 3L; + private final int size = 4; + private final int hashCode = 5; + private final int timeDiff = 6; + private final int itemIndex = 7; + + @Test + public void indexItemConstructorTest() { + IndexItem indexItem = new IndexItem(topicId, queueId, offset, size, hashCode, timeDiff, itemIndex); + + Assert.assertEquals(topicId, indexItem.getTopicId()); + Assert.assertEquals(queueId, indexItem.getQueueId()); + Assert.assertEquals(offset, indexItem.getOffset()); + Assert.assertEquals(size, indexItem.getSize()); + Assert.assertEquals(hashCode, indexItem.getHashCode()); + Assert.assertEquals(timeDiff, indexItem.getTimeDiff()); + Assert.assertEquals(itemIndex, indexItem.getItemIndex()); + } + + @Test + public void byteBufferConstructorTest() { + ByteBuffer byteBuffer = ByteBuffer.allocate(IndexItem.INDEX_ITEM_SIZE); + byteBuffer.putInt(hashCode); + byteBuffer.putInt(topicId); + byteBuffer.putInt(queueId); + byteBuffer.putLong(offset); + byteBuffer.putInt(size); + byteBuffer.putInt(timeDiff); + byteBuffer.putInt(itemIndex); + + byte[] bytes = byteBuffer.array(); + IndexItem indexItem = new IndexItem(bytes); + + Assert.assertEquals(topicId, indexItem.getTopicId()); + Assert.assertEquals(queueId, indexItem.getQueueId()); + Assert.assertEquals(offset, indexItem.getOffset()); + Assert.assertEquals(size, indexItem.getSize()); + Assert.assertEquals(hashCode, indexItem.getHashCode()); + Assert.assertEquals(timeDiff, indexItem.getTimeDiff()); + Assert.assertEquals(itemIndex, indexItem.getItemIndex()); + Assert.assertNotNull(indexItem.toString()); + + Exception exception = null; + try { + new IndexItem(null); + } catch (Exception e) { + exception = e; + } + Assert.assertNotNull(exception); + } + + @Test + public void getByteBufferTest() { + IndexItem indexItem = new IndexItem(topicId, queueId, offset, size, hashCode, timeDiff, itemIndex); + ByteBuffer byteBuffer = indexItem.getByteBuffer(); + Assert.assertEquals(hashCode, byteBuffer.getInt(0)); + Assert.assertEquals(topicId, byteBuffer.getInt(4)); + Assert.assertEquals(queueId, byteBuffer.getInt(8)); + Assert.assertEquals(offset, byteBuffer.getLong(12)); + Assert.assertEquals(size, byteBuffer.getInt(20)); + Assert.assertEquals(timeDiff, byteBuffer.getInt(24)); + Assert.assertEquals(itemIndex, byteBuffer.getInt(28)); + } +} \ No newline at end of file diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/index/IndexStoreFileTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/index/IndexStoreFileTest.java new file mode 100644 index 0000000..d19b562 --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/index/IndexStoreFileTest.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.tieredstore.index; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.MessageStoreExecutor; +import org.apache.rocketmq.tieredstore.common.AppendResult; +import org.apache.rocketmq.tieredstore.common.FileSegmentType; +import org.apache.rocketmq.tieredstore.provider.FileSegment; +import org.apache.rocketmq.tieredstore.provider.PosixFileSegment; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtilTest; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class IndexStoreFileTest { + + private static final String TOPIC_NAME = "TopicTest"; + private static final int TOPIC_ID = 123; + private static final int QUEUE_ID = 2; + private static final long MESSAGE_OFFSET = 666L; + private static final int MESSAGE_SIZE = 1024; + private static final String KEY = "MessageKey"; + private static final Set KEY_SET = Collections.singleton(KEY); + + private String filePath; + private MessageStoreConfig storeConfig; + private IndexStoreFile indexStoreFile; + + @Before + public void init() throws IOException { + filePath = UUID.randomUUID().toString().replace("-", "").substring(0, 8); + String directory = Paths.get(System.getProperty("user.home"), "store_test", filePath).toString(); + storeConfig = new MessageStoreConfig(); + storeConfig.setStorePathRootDir(directory); + storeConfig.setTieredStoreFilePath(directory); + storeConfig.setTieredStoreIndexFileMaxHashSlotNum(5); + storeConfig.setTieredStoreIndexFileMaxIndexNum(20); + storeConfig.setTieredBackendServiceProvider("org.apache.rocketmq.tieredstore.provider.PosixFileSegment"); + indexStoreFile = new IndexStoreFile(storeConfig, System.currentTimeMillis()); + } + + @After + public void shutdown() { + if (this.indexStoreFile != null) { + this.indexStoreFile.shutdown(); + this.indexStoreFile.destroy(); + } + MessageStoreUtilTest.deleteStoreDirectory(storeConfig.getTieredStoreFilePath()); + } + + @Test + public void testIndexHeaderConstants() { + Assert.assertEquals(0, IndexStoreFile.INDEX_MAGIC_CODE); + Assert.assertEquals(4, IndexStoreFile.INDEX_BEGIN_TIME_STAMP); + Assert.assertEquals(12, IndexStoreFile.INDEX_END_TIME_STAMP); + Assert.assertEquals(20, IndexStoreFile.INDEX_SLOT_COUNT); + Assert.assertEquals(24, IndexStoreFile.INDEX_ITEM_INDEX); + Assert.assertEquals(28, IndexStoreFile.INDEX_HEADER_SIZE); + Assert.assertEquals(0xCCDDEEFF ^ 1880681586 + 4, IndexStoreFile.BEGIN_MAGIC_CODE); + Assert.assertEquals(0xCCDDEEFF ^ 1880681586 + 8, IndexStoreFile.END_MAGIC_CODE); + } + + @Test + public void basicMethodTest() throws IOException { + long timestamp = System.currentTimeMillis(); + IndexStoreFile localFile = new IndexStoreFile(storeConfig, timestamp); + Assert.assertEquals(timestamp, localFile.getTimestamp()); + + // test file status + Assert.assertEquals(IndexFile.IndexStatusEnum.UNSEALED, localFile.getFileStatus()); + localFile.doCompaction(); + Assert.assertEquals(IndexFile.IndexStatusEnum.SEALED, localFile.getFileStatus()); + + // test hash + Assert.assertEquals("TopicTest#MessageKey", localFile.buildKey(TOPIC_NAME, KEY)); + Assert.assertEquals(638347386, indexStoreFile.hashCode(localFile.buildKey(TOPIC_NAME, KEY))); + + // test calculate position + long headerSize = IndexStoreFile.INDEX_HEADER_SIZE; + Assert.assertEquals(headerSize + Long.BYTES * 2, indexStoreFile.getSlotPosition(2)); + Assert.assertEquals(headerSize + Long.BYTES * 5, indexStoreFile.getSlotPosition(5)); + Assert.assertEquals(headerSize + Long.BYTES * 5 + IndexItem.INDEX_ITEM_SIZE * 2, + indexStoreFile.getItemPosition(2)); + Assert.assertEquals(headerSize + Long.BYTES * 5 + IndexItem.INDEX_ITEM_SIZE * 5, + indexStoreFile.getItemPosition(5)); + } + + @Test + public void basicPutGetTest() { + long timestamp = indexStoreFile.getTimestamp(); + + // check metadata + Assert.assertEquals(timestamp, indexStoreFile.getTimestamp()); + Assert.assertEquals(0, indexStoreFile.getEndTimestamp()); + Assert.assertEquals(0, indexStoreFile.getIndexItemCount()); + Assert.assertEquals(0, indexStoreFile.getHashSlotCount()); + + // not put success + Assert.assertEquals(AppendResult.UNKNOWN_ERROR, indexStoreFile.putKey( + null, TOPIC_ID, QUEUE_ID, KEY_SET, MESSAGE_OFFSET, MESSAGE_SIZE, timestamp)); + Assert.assertEquals(AppendResult.SUCCESS, indexStoreFile.putKey( + TOPIC_NAME, TOPIC_ID, QUEUE_ID, null, MESSAGE_OFFSET, MESSAGE_SIZE, timestamp)); + Assert.assertEquals(AppendResult.SUCCESS, indexStoreFile.putKey( + TOPIC_NAME, TOPIC_ID, QUEUE_ID, Collections.emptySet(), MESSAGE_OFFSET, MESSAGE_SIZE, timestamp)); + + // first item is invalid + for (int i = 0; i < storeConfig.getTieredStoreIndexFileMaxIndexNum() - 2; i++) { + Assert.assertEquals(AppendResult.SUCCESS, indexStoreFile.putKey( + TOPIC_NAME, TOPIC_ID, QUEUE_ID, KEY_SET, MESSAGE_OFFSET, MESSAGE_SIZE, timestamp)); + Assert.assertEquals(timestamp, indexStoreFile.getTimestamp()); + Assert.assertEquals(timestamp, indexStoreFile.getEndTimestamp()); + Assert.assertEquals(1, indexStoreFile.getHashSlotCount()); + Assert.assertEquals(i + 1, indexStoreFile.getIndexItemCount()); + } + + Assert.assertEquals(AppendResult.SUCCESS, indexStoreFile.putKey( + TOPIC_NAME, TOPIC_ID, QUEUE_ID, KEY_SET, MESSAGE_OFFSET, MESSAGE_SIZE, timestamp)); + Assert.assertEquals(AppendResult.FILE_FULL, indexStoreFile.putKey( + TOPIC_NAME, TOPIC_ID, QUEUE_ID, KEY_SET, MESSAGE_OFFSET, MESSAGE_SIZE, timestamp)); + + Assert.assertEquals(timestamp, indexStoreFile.getTimestamp()); + Assert.assertEquals(timestamp, indexStoreFile.getEndTimestamp()); + Assert.assertEquals(1, indexStoreFile.getHashSlotCount()); + Assert.assertEquals(storeConfig.getTieredStoreIndexFileMaxIndexNum() - 1, indexStoreFile.getIndexItemCount()); + } + + @Test + public void differentKeyPutTest() { + long timestamp = indexStoreFile.getTimestamp(); + for (int i = 0; i < 5; i++) { + for (int j = 0; j < 3; j++) { + Assert.assertEquals(AppendResult.SUCCESS, indexStoreFile.putKey( + TOPIC_NAME + i, TOPIC_ID, QUEUE_ID, KEY_SET, MESSAGE_OFFSET, MESSAGE_SIZE, timestamp)); + } + } + Assert.assertEquals(timestamp, indexStoreFile.getTimestamp()); + Assert.assertEquals(timestamp, indexStoreFile.getEndTimestamp()); + Assert.assertEquals(5, indexStoreFile.getHashSlotCount()); + Assert.assertEquals(5 * 3, indexStoreFile.getIndexItemCount()); + } + + @Test + public void concurrentPutTest() throws InterruptedException { + long timestamp = indexStoreFile.getTimestamp(); + + ExecutorService executorService = Executors.newFixedThreadPool( + 4, new ThreadFactoryImpl("ConcurrentPutGetTest")); + + // first item is invalid + int indexCount = storeConfig.getTieredStoreIndexFileMaxIndexNum() - 1; + CountDownLatch latch = new CountDownLatch(indexCount); + for (int i = 0; i < indexCount; i++) { + executorService.submit(() -> { + Assert.assertEquals(AppendResult.SUCCESS, indexStoreFile.putKey( + TOPIC_NAME, TOPIC_ID, QUEUE_ID, KEY_SET, MESSAGE_OFFSET, MESSAGE_SIZE, timestamp)); + try { + Thread.sleep(100); + } catch (InterruptedException ignored) { + } + latch.countDown(); + }); + } + latch.await(); + + executorService.shutdown(); + Assert.assertEquals(AppendResult.FILE_FULL, indexStoreFile.putKey( + TOPIC_NAME, TOPIC_ID, QUEUE_ID, KEY_SET, MESSAGE_OFFSET, MESSAGE_SIZE, timestamp)); + Assert.assertEquals(indexCount, indexStoreFile.getIndexItemCount()); + } + + @Test + public void recoverFileTest() throws IOException { + int indexCount = 10; + long timestamp = indexStoreFile.getTimestamp(); + for (int i = 0; i < indexCount; i++) { + Assert.assertEquals(AppendResult.SUCCESS, indexStoreFile.putKey( + TOPIC_NAME, TOPIC_ID, QUEUE_ID, KEY_SET, MESSAGE_OFFSET, MESSAGE_SIZE, timestamp)); + } + indexStoreFile.shutdown(); + Assert.assertEquals(indexCount, indexStoreFile.getIndexItemCount()); + indexStoreFile = new IndexStoreFile(storeConfig, timestamp); + Assert.assertEquals(indexCount, indexStoreFile.getIndexItemCount()); + } + + @Test + public void doCompactionTest() { + long timestamp = indexStoreFile.getTimestamp(); + for (int i = 0; i < 10; i++) { + Assert.assertEquals(AppendResult.SUCCESS, indexStoreFile.putKey( + TOPIC_NAME, TOPIC_ID, QUEUE_ID, KEY_SET, MESSAGE_OFFSET, MESSAGE_SIZE, timestamp)); + } + + ByteBuffer byteBuffer = indexStoreFile.doCompaction(); + FileSegment fileSegment = new PosixFileSegment( + storeConfig, FileSegmentType.INDEX, filePath, 0L, new MessageStoreExecutor()); + fileSegment.append(byteBuffer, timestamp); + fileSegment.commitAsync().join(); + Assert.assertEquals(byteBuffer.limit(), fileSegment.getSize()); + fileSegment.destroyFile(); + } + + @Test + public void queryAsyncFromUnsealedFileTest() throws Exception { + long timestamp = indexStoreFile.getTimestamp(); + for (int i = 0; i < 5; i++) { + for (int j = 0; j < 3; j++) { + Assert.assertEquals(AppendResult.SUCCESS, indexStoreFile.putKey(TOPIC_NAME + i, + TOPIC_ID, QUEUE_ID, KEY_SET, MESSAGE_OFFSET, MESSAGE_SIZE, System.currentTimeMillis())); + } + } + List itemList = indexStoreFile.queryAsync( + TOPIC_NAME + "1", KEY, 64, timestamp, System.currentTimeMillis()).get(); + Assert.assertEquals(3, itemList.size()); + } + + @Test + public void queryAsyncFromSegmentFileTest() throws ExecutionException, InterruptedException { + long timestamp = indexStoreFile.getTimestamp(); + for (int i = 0; i < 5; i++) { + for (int j = 0; j < 3; j++) { + Assert.assertEquals(AppendResult.SUCCESS, indexStoreFile.putKey(TOPIC_NAME + i, + TOPIC_ID, QUEUE_ID, KEY_SET, MESSAGE_OFFSET, MESSAGE_SIZE, System.currentTimeMillis())); + } + } + + ByteBuffer byteBuffer = indexStoreFile.doCompaction(); + FileSegment fileSegment = new PosixFileSegment( + storeConfig, FileSegmentType.INDEX, filePath, 0L, new MessageStoreExecutor()); + fileSegment.append(byteBuffer, timestamp); + fileSegment.commitAsync().join(); + Assert.assertEquals(byteBuffer.limit(), fileSegment.getSize()); + indexStoreFile.destroy(); + + indexStoreFile = new IndexStoreFile(storeConfig, fileSegment); + + // change topic + List itemList = indexStoreFile.queryAsync( + TOPIC_NAME, KEY, 64, timestamp, System.currentTimeMillis()).get(); + Assert.assertEquals(0, itemList.size()); + + // change key + itemList = indexStoreFile.queryAsync( + TOPIC_NAME, KEY + "1", 64, timestamp, System.currentTimeMillis()).get(); + Assert.assertEquals(0, itemList.size()); + + itemList = indexStoreFile.queryAsync( + TOPIC_NAME + "1", KEY, 64, timestamp, System.currentTimeMillis()).get(); + Assert.assertEquals(3, itemList.size()); + } +} \ No newline at end of file diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/index/IndexStoreServiceBenchTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/index/IndexStoreServiceBenchTest.java new file mode 100644 index 0000000..fcb2840 --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/index/IndexStoreServiceBenchTest.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.tieredstore.index; + +import com.google.common.base.Stopwatch; +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.LongAdder; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.common.AppendResult; +import org.apache.rocketmq.tieredstore.file.FlatFileFactory; +import org.apache.rocketmq.tieredstore.metadata.DefaultMetadataStore; +import org.apache.rocketmq.tieredstore.metadata.MetadataStore; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtil; +import org.junit.Assert; +import org.junit.Ignore; +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 org.openjdk.jmh.results.format.ResultFormatType; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Ignore +@State(Scope.Benchmark) +@Fork(value = 1, jvmArgs = {"-Djava.net.preferIPv4Stack=true", "-Djmh.rmi.port=1099"}) +public class IndexStoreServiceBenchTest { + + private static final Logger log = LoggerFactory.getLogger(MessageStoreUtil.TIERED_STORE_LOGGER_NAME); + private static final String TOPIC_NAME = "TopicTest"; + private MessageStoreConfig storeConfig; + private IndexStoreService indexStoreService; + private final LongAdder failureCount = new LongAdder(); + + @Setup + public void init() throws ClassNotFoundException, NoSuchMethodException { + String storePath = Paths.get(System.getProperty("user.home"), "store_test", "index").toString(); + UtilAll.deleteFile(new File(storePath)); + UtilAll.deleteFile(new File("./e96d41b2_IndexService")); + storeConfig = new MessageStoreConfig(); + storeConfig.setBrokerClusterName("IndexService"); + storeConfig.setBrokerName("IndexServiceBroker"); + storeConfig.setStorePathRootDir(storePath); + storeConfig.setTieredBackendServiceProvider("org.apache.rocketmq.tieredstore.provider.PosixFileSegment"); + storeConfig.setTieredStoreIndexFileMaxHashSlotNum(500 * 1000); + storeConfig.setTieredStoreIndexFileMaxIndexNum(2000 * 1000); + MetadataStore metadataStore = new DefaultMetadataStore(storeConfig); + FlatFileFactory flatFileFactory = new FlatFileFactory(metadataStore, storeConfig); + indexStoreService = new IndexStoreService(flatFileFactory, storePath); + indexStoreService.start(); + } + + @TearDown() + public void shutdown() throws IOException { + indexStoreService.shutdown(); + indexStoreService.destroy(); + } + + //@Benchmark + @Threads(2) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + @Warmup(iterations = 1, time = 1) + @Measurement(iterations = 1, time = 1) + public void doPutThroughputBenchmark() { + for (int i = 0; i < 100; i++) { + AppendResult result = indexStoreService.putKey( + TOPIC_NAME, 123, 2, Collections.singleton(String.valueOf(i)), + i * 100L, i * 100, System.currentTimeMillis()); + if (AppendResult.SUCCESS.equals(result)) { + failureCount.increment(); + } + } + } + + @Threads(1) + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.SECONDS) + @Warmup(iterations = 0) + @Measurement(iterations = 1, time = 1) + public void doGetThroughputBenchmark() throws ExecutionException, InterruptedException { + for (int j = 0; j < 10; j++) { + for (int i = 0; i < storeConfig.getTieredStoreIndexFileMaxIndexNum(); i++) { + indexStoreService.putKey( + "TopicTest", 123, j, Collections.singleton(String.valueOf(i)), + i * 100L, i * 100, System.currentTimeMillis()); + } + } + + int queryCount = 100 * 10000; + Stopwatch stopwatch = Stopwatch.createStarted(); + for (int i = 0; i < queryCount; i++) { + List indexItems = indexStoreService.queryAsync(TOPIC_NAME, String.valueOf(i), + 20, 0, System.currentTimeMillis()).get(); + Assert.assertEquals(10, indexItems.size()); + + List indexItems2 = indexStoreService.queryAsync(TOPIC_NAME, String.valueOf(i), + 5, 0, System.currentTimeMillis()).get(); + Assert.assertEquals(5, indexItems2.size()); + } + log.info("DoGetThroughputBenchmark test cost: {}ms", stopwatch.elapsed(TimeUnit.MILLISECONDS)); + } + + public static void main(String[] args) throws Exception { + Options opt = new OptionsBuilder() + .include(IndexStoreServiceBenchTest.class.getSimpleName()) + .warmupIterations(0) + .measurementIterations(1) + .result("result.json") + .resultFormat(ResultFormatType.JSON) + .build(); + new Runner(opt).run(); + } +} diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/index/IndexStoreServiceTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/index/IndexStoreServiceTest.java new file mode 100644 index 0000000..7b881dd --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/index/IndexStoreServiceTest.java @@ -0,0 +1,354 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tieredstore.index; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +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.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.store.logfile.DefaultMappedFile; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.common.AppendResult; +import org.apache.rocketmq.tieredstore.file.FlatAppendFile; +import org.apache.rocketmq.tieredstore.file.FlatFileFactory; +import org.apache.rocketmq.tieredstore.metadata.DefaultMetadataStore; +import org.apache.rocketmq.tieredstore.metadata.MetadataStore; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtil; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtilTest; +import org.awaitility.Awaitility; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.awaitility.Awaitility.await; + +public class IndexStoreServiceTest { + + private static final Logger log = LoggerFactory.getLogger(MessageStoreUtil.TIERED_STORE_LOGGER_NAME); + + private static final String TOPIC_NAME = "TopicTest"; + private static final int TOPIC_ID = 123; + private static final int QUEUE_ID = 2; + private static final long MESSAGE_OFFSET = 666; + private static final int MESSAGE_SIZE = 1024; + private static final Set KEY_SET = Collections.singleton("MessageKey"); + + private String filePath; + private MessageStoreConfig storeConfig; + private FlatFileFactory fileAllocator; + private IndexStoreService indexService; + + @Before + public void init() throws IOException, ClassNotFoundException, NoSuchMethodException { + filePath = UUID.randomUUID().toString().replace("-", "").substring(0, 8); + String directory = Paths.get(System.getProperty("user.home"), "store_test", filePath).toString(); + storeConfig = new MessageStoreConfig(); + storeConfig.setStorePathRootDir(directory); + storeConfig.setTieredStoreFilePath(directory); + storeConfig.setTieredStoreIndexFileMaxHashSlotNum(5); + storeConfig.setTieredStoreIndexFileMaxIndexNum(20); + storeConfig.setTieredBackendServiceProvider("org.apache.rocketmq.tieredstore.provider.PosixFileSegment"); + MetadataStore metadataStore = new DefaultMetadataStore(storeConfig); + fileAllocator = new FlatFileFactory(metadataStore, storeConfig); + } + + @After + public void shutdown() { + if (indexService != null) { + indexService.shutdown(); + indexService.destroy(); + } + MessageStoreUtilTest.deleteStoreDirectory(storeConfig.getTieredStoreFilePath()); + } + + @Test + public void basicServiceTest() throws InterruptedException { + indexService = new IndexStoreService(fileAllocator, filePath); + indexService.start(); + for (int i = 0; i < 50; i++) { + Assert.assertEquals(AppendResult.SUCCESS, indexService.putKey( + TOPIC_NAME, TOPIC_ID, QUEUE_ID, KEY_SET, i * 100, MESSAGE_SIZE, System.currentTimeMillis())); + TimeUnit.MILLISECONDS.sleep(1); + } + ConcurrentSkipListMap timeStoreTable = indexService.getTimeStoreTable(); + Assert.assertEquals(3, timeStoreTable.size()); + } + + @Test + public void doConvertOldFormatTest() throws IOException { + indexService = new IndexStoreService(fileAllocator, filePath); + indexService.start(); + long timestamp = indexService.getTimeStoreTable().firstKey(); + Assert.assertEquals(AppendResult.SUCCESS, indexService.putKey( + TOPIC_NAME, TOPIC_ID, QUEUE_ID, KEY_SET, MESSAGE_OFFSET, MESSAGE_SIZE, timestamp)); + indexService.shutdown(); + + File file = new File(Paths.get(filePath, IndexStoreService.FILE_DIRECTORY_NAME, String.valueOf(timestamp)).toString()); + DefaultMappedFile mappedFile = new DefaultMappedFile(file.getName(), (int) file.length()); + mappedFile.renameTo(String.valueOf(new File(file.getParent(), "0000"))); + mappedFile.shutdown(10 * 1000); + + indexService = new IndexStoreService(fileAllocator, filePath); + indexService.start(); + ConcurrentSkipListMap timeStoreTable = indexService.getTimeStoreTable(); + Assert.assertEquals(2, timeStoreTable.size()); + Assert.assertEquals(Long.valueOf(timestamp), timeStoreTable.firstKey()); + mappedFile.destroy(10 * 1000); + } + + @Test + public void concurrentPutTest() throws InterruptedException { + ExecutorService executorService = Executors.newFixedThreadPool( + 4, new ThreadFactoryImpl("ConcurrentPutTest")); + storeConfig.setTieredStoreIndexFileMaxHashSlotNum(500); + storeConfig.setTieredStoreIndexFileMaxIndexNum(2000); + indexService = new IndexStoreService(fileAllocator, filePath); + indexService.start(); + long timestamp = System.currentTimeMillis(); + + // first item is invalid + AtomicInteger success = new AtomicInteger(); + int indexCount = 5000; + CountDownLatch latch = new CountDownLatch(indexCount); + for (int i = 0; i < indexCount; i++) { + final int index = i; + executorService.submit(() -> { + try { + AppendResult result = indexService.putKey( + TOPIC_NAME, TOPIC_ID, QUEUE_ID, Collections.singleton(String.valueOf(index)), + index * 100, MESSAGE_SIZE, timestamp + index); + if (AppendResult.SUCCESS.equals(result)) { + success.incrementAndGet(); + } + } catch (Exception e) { + log.error("ConcurrentPutTest error", e); + } finally { + latch.countDown(); + } + }); + } + Assert.assertTrue(latch.await(10, TimeUnit.SECONDS)); + Assert.assertEquals(3, indexService.getTimeStoreTable().size()); + executorService.shutdown(); + } + + @Test + public void doCompactionTest() throws InterruptedException { + concurrentPutTest(); + IndexFile indexFile = indexService.getNextSealedFile(); + Assert.assertEquals(IndexFile.IndexStatusEnum.SEALED, indexFile.getFileStatus()); + + indexService.doCompactThenUploadFile(indexFile); + indexService.setCompactTimestamp(indexFile.getTimestamp()); + indexFile.destroy(); + + List files = new ArrayList<>(indexService.getTimeStoreTable().values()); + Assert.assertEquals(IndexFile.IndexStatusEnum.UPLOAD, files.get(0).getFileStatus()); + Assert.assertEquals(IndexFile.IndexStatusEnum.SEALED, files.get(1).getFileStatus()); + Assert.assertEquals(IndexFile.IndexStatusEnum.UNSEALED, files.get(2).getFileStatus()); + + indexFile = indexService.getNextSealedFile(); + indexService.doCompactThenUploadFile(indexFile); + indexService.setCompactTimestamp(indexFile.getTimestamp()); + files = new ArrayList<>(indexService.getTimeStoreTable().values()); + Assert.assertEquals(IndexFile.IndexStatusEnum.UPLOAD, files.get(0).getFileStatus()); + Assert.assertEquals(IndexFile.IndexStatusEnum.UPLOAD, files.get(1).getFileStatus()); + Assert.assertEquals(IndexFile.IndexStatusEnum.UNSEALED, files.get(2).getFileStatus()); + + indexFile = indexService.getNextSealedFile(); + Assert.assertNull(indexFile); + files = new ArrayList<>(indexService.getTimeStoreTable().values()); + Assert.assertEquals(IndexFile.IndexStatusEnum.UPLOAD, files.get(0).getFileStatus()); + Assert.assertEquals(IndexFile.IndexStatusEnum.UPLOAD, files.get(1).getFileStatus()); + Assert.assertEquals(IndexFile.IndexStatusEnum.UNSEALED, files.get(2).getFileStatus()); + } + + @Test + public void runServiceTest() throws InterruptedException { + concurrentPutTest(); + indexService.start(); + await().atMost(Duration.ofMinutes(1)).pollInterval(Duration.ofSeconds(1)).until(() -> { + boolean result = true; + ArrayList files = new ArrayList<>(indexService.getTimeStoreTable().values()); + result &= IndexFile.IndexStatusEnum.UPLOAD.equals(files.get(0).getFileStatus()); + result &= IndexFile.IndexStatusEnum.UPLOAD.equals(files.get(1).getFileStatus()); + result &= IndexFile.IndexStatusEnum.UNSEALED.equals(files.get(2).getFileStatus()); + return result; + }); + } + + @Test + public void deleteFileTest() throws InterruptedException, IllegalAccessException { + indexService = new IndexStoreService(fileAllocator, filePath); + indexService.start(); + + for (int i = 0; i < 2 * 20; i++) { + AppendResult result = indexService.putKey( + TOPIC_NAME, TOPIC_ID, QUEUE_ID, Collections.singleton(String.valueOf(i)), + i * 100L, MESSAGE_SIZE, System.currentTimeMillis()); + Assert.assertEquals(AppendResult.SUCCESS, result); + TimeUnit.MILLISECONDS.sleep(1); + } + + indexService.wakeup(); + Awaitility.await().until(() -> { + int tableSize = (int) indexService.getTimeStoreTable().entrySet().stream() + .filter(entry -> IndexFile.IndexStatusEnum.UPLOAD.equals(entry.getValue().getFileStatus())) + .count(); + return tableSize == 2; + }); + + long timestamp = indexService.getTimeStoreTable().firstEntry().getValue().getEndTimestamp(); + FlatAppendFile flatAppendFile = (FlatAppendFile) + FieldUtils.readField(indexService, "flatAppendFile", true); + + indexService.destroyExpiredFile(timestamp); + Assert.assertEquals(2, flatAppendFile.getFileSegmentList().size()); + Assert.assertEquals(3, indexService.getTimeStoreTable().size()); + indexService.destroyExpiredFile(timestamp + 1); + Assert.assertEquals(1, flatAppendFile.getFileSegmentList().size()); + Assert.assertEquals(2, indexService.getTimeStoreTable().size()); + } + + @Test + public void restartServiceTest() throws InterruptedException { + indexService = new IndexStoreService(fileAllocator, filePath); + indexService.start(); + for (int i = 0; i < 20; i++) { + AppendResult result = indexService.putKey( + TOPIC_NAME, TOPIC_ID, QUEUE_ID, Collections.singleton(String.valueOf(i)), + i * 100L, MESSAGE_SIZE, System.currentTimeMillis()); + Assert.assertEquals(AppendResult.SUCCESS, result); + TimeUnit.MILLISECONDS.sleep(1); + } + long timestamp = indexService.getTimeStoreTable().firstKey(); + indexService.shutdown(); + + indexService = new IndexStoreService(fileAllocator, filePath); + indexService.start(); + Assert.assertEquals(timestamp, indexService.getTimeStoreTable().firstKey().longValue()); + await().atMost(Duration.ofMinutes(1)).pollInterval(Duration.ofSeconds(1)).until(() -> { + ArrayList files = new ArrayList<>(indexService.getTimeStoreTable().values()); + return IndexFile.IndexStatusEnum.UPLOAD.equals(files.get(0).getFileStatus()); + }); + indexService.shutdown(); + + indexService = new IndexStoreService(fileAllocator, filePath); + indexService.start(); + Assert.assertEquals(timestamp, indexService.getTimeStoreTable().firstKey().longValue()); + Assert.assertEquals(4, indexService.getTimeStoreTable().size()); + Assert.assertEquals(IndexFile.IndexStatusEnum.UPLOAD, + indexService.getTimeStoreTable().firstEntry().getValue().getFileStatus()); + } + + @Test + public void queryFromFileTest() throws InterruptedException, ExecutionException { + long timestamp = System.currentTimeMillis(); + indexService = new IndexStoreService(fileAllocator, filePath); + indexService.start(); + + // three files, echo contains 19 items + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 20 - 1; j++) { + AppendResult result = indexService.putKey( + TOPIC_NAME, TOPIC_ID, QUEUE_ID, Collections.singleton(String.valueOf(j)), + i * 100L + j, MESSAGE_SIZE, System.currentTimeMillis()); + Assert.assertEquals(AppendResult.SUCCESS, result); + TimeUnit.MILLISECONDS.sleep(1); + } + } + + ArrayList files = new ArrayList<>(indexService.getTimeStoreTable().values()); + Assert.assertEquals(3, files.size()); + + for (int i = 0; i < 3; i++) { + List indexItems = indexService.queryAsync( + TOPIC_NAME, String.valueOf(1), 1, timestamp, System.currentTimeMillis()).get(); + Assert.assertEquals(1, indexItems.size()); + + indexItems = indexService.queryAsync( + TOPIC_NAME, String.valueOf(1), 3, timestamp, System.currentTimeMillis()).get(); + Assert.assertEquals(3, indexItems.size()); + + indexItems = indexService.queryAsync( + TOPIC_NAME, String.valueOf(1), 5, timestamp, System.currentTimeMillis()).get(); + Assert.assertEquals(3, indexItems.size()); + } + } + + @Test + public void concurrentGetTest() throws InterruptedException { + storeConfig.setTieredStoreIndexFileMaxIndexNum(2000); + indexService = new IndexStoreService(fileAllocator, filePath); + indexService.start(); + + int fileCount = 10; + for (int j = 0; j < fileCount; j++) { + for (int i = 0; i < storeConfig.getTieredStoreIndexFileMaxIndexNum(); i++) { + indexService.putKey(TOPIC_NAME, TOPIC_ID, j, Collections.singleton(String.valueOf(i)), + i * 100L, i * 100, System.currentTimeMillis()); + } + TimeUnit.MILLISECONDS.sleep(1); + } + + CountDownLatch latch = new CountDownLatch(fileCount * 3); + AtomicBoolean result = new AtomicBoolean(true); + ExecutorService executorService = Executors.newFixedThreadPool( + 4, new ThreadFactoryImpl("ConcurrentGetTest")); + + for (int i = 0; i < fileCount; i++) { + int finalI = i; + executorService.submit(() -> { + for (int j = 1; j <= 3; j++) { + try { + List indexItems = indexService.queryAsync( + TOPIC_NAME, String.valueOf(finalI), j * 5, 0, System.currentTimeMillis()).get(); + if (Math.min(fileCount, j * 5) != indexItems.size()) { + result.set(false); + } + } catch (Exception e) { + result.set(false); + } finally { + latch.countDown(); + } + } + }); + } + + Assert.assertTrue(latch.await(15, TimeUnit.SECONDS)); + executorService.shutdown(); + Assert.assertTrue(result.get()); + } +} \ No newline at end of file diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/metadata/DefaultMetadataStoreTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/metadata/DefaultMetadataStoreTest.java new file mode 100644 index 0000000..7a33903 --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/metadata/DefaultMetadataStoreTest.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.tieredstore.metadata; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.common.FileSegmentType; +import org.apache.rocketmq.tieredstore.metadata.entity.FileSegmentMetadata; +import org.apache.rocketmq.tieredstore.metadata.entity.QueueMetadata; +import org.apache.rocketmq.tieredstore.metadata.entity.TopicMetadata; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtil; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtilTest; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class DefaultMetadataStoreTest { + + private final String storePath = MessageStoreUtilTest.getRandomStorePath(); + private MessageQueue mq0; + private MessageQueue mq1; + private MessageQueue mq2; + private MessageStoreConfig storeConfig; + private MetadataStore metadataStore; + + @Before + public void init() { + storeConfig = new MessageStoreConfig(); + storeConfig.setBrokerName("brokerName"); + storeConfig.setStorePathRootDir(storePath); + mq0 = new MessageQueue("MetadataStoreTest0", storeConfig.getBrokerName(), 0); + mq1 = new MessageQueue("MetadataStoreTest1", storeConfig.getBrokerName(), 0); + mq2 = new MessageQueue("MetadataStoreTest1", storeConfig.getBrokerName(), 1); + metadataStore = new DefaultMetadataStore(storeConfig); + } + + @After + public void shutdown() throws IOException { + metadataStore.destroy(); + MessageStoreUtilTest.deleteStoreDirectory(storePath); + } + + @Test + public void testQueue() { + QueueMetadata queueMetadata = metadataStore.getQueue(mq0); + Assert.assertNull(queueMetadata); + + queueMetadata = metadataStore.addQueue(mq0, -1); + Assert.assertEquals(queueMetadata.getMinOffset(), -1); + Assert.assertEquals(queueMetadata.getMaxOffset(), -1); + + long currentTimeMillis = System.currentTimeMillis(); + queueMetadata.setMinOffset(0); + queueMetadata.setMaxOffset(0); + metadataStore.updateQueue(queueMetadata); + queueMetadata = metadataStore.getQueue(mq0); + Assert.assertTrue(Objects.requireNonNull(queueMetadata).getUpdateTimestamp() >= currentTimeMillis); + Assert.assertEquals(queueMetadata.getMinOffset(), 0); + Assert.assertEquals(queueMetadata.getMaxOffset(), 0); + + MessageQueue mq2 = new MessageQueue(mq0.getTopic(), storeConfig.getBrokerName(), 2); + metadataStore.addQueue(mq2, 1); + AtomicInteger i = new AtomicInteger(0); + metadataStore.iterateQueue(mq0.getTopic(), metadata -> { + Assert.assertEquals(i.get(), metadata.getMinOffset()); + i.getAndIncrement(); + }); + Assert.assertEquals(i.get(), 2); + + metadataStore.deleteQueue(mq0); + queueMetadata = metadataStore.getQueue(mq0); + Assert.assertNull(queueMetadata); + } + + @Test + public void testTopic() { + TopicMetadata topicMetadata = metadataStore.getTopic(mq0.getTopic()); + Assert.assertNull(topicMetadata); + + metadataStore.addTopic(mq0.getTopic(), 2); + topicMetadata = metadataStore.getTopic(mq0.getTopic()); + Assert.assertEquals(mq0.getTopic(), Objects.requireNonNull(topicMetadata).getTopic()); + Assert.assertEquals(topicMetadata.getStatus(), 0); + Assert.assertEquals(topicMetadata.getReserveTime(), 2); + Assert.assertEquals(topicMetadata.getTopicId(), 0); + + topicMetadata.setStatus(1); + topicMetadata.setReserveTime(0); + metadataStore.updateTopic(topicMetadata); + topicMetadata = metadataStore.getTopic(mq0.getTopic()); + Assert.assertNotNull(topicMetadata); + Assert.assertEquals(topicMetadata.getStatus(), 1); + Assert.assertEquals(topicMetadata.getReserveTime(), 0); + + String topic1 = mq0.getTopic() + "1"; + metadataStore.addTopic(topic1, 1); + TopicMetadata topicMetadata1 = metadataStore.getTopic(topic1); + Assert.assertNotNull(topicMetadata1); + topicMetadata1.setStatus(2); + metadataStore.updateTopic(topicMetadata1); + + String topic2 = mq0.getTopic() + "2"; + metadataStore.addTopic(topic2, 2); + TopicMetadata topicMetadata2 = metadataStore.getTopic(topic2); + Assert.assertNotNull(topicMetadata2); + topicMetadata2.setStatus(3); + metadataStore.updateTopic(topicMetadata2); + + AtomicInteger n = new AtomicInteger(); + metadataStore.iterateTopic(metadata -> { + long i = metadata.getReserveTime(); + Assert.assertEquals(metadata.getTopicId(), i); + Assert.assertEquals(metadata.getStatus(), i + 1); + if (i == 2) { + metadataStore.deleteTopic(metadata.getTopic()); + } + n.getAndIncrement(); + }); + + Assert.assertEquals(3, n.get()); + Assert.assertNull(metadataStore.getTopic(topic2)); + Assert.assertNotNull(metadataStore.getTopic(mq0.getTopic())); + Assert.assertNotNull(metadataStore.getTopic(topic1)); + } + + private long countFileSegment(MetadataStore metadataStore) { + AtomicLong count = new AtomicLong(); + metadataStore.iterateFileSegment(segmentMetadata -> count.incrementAndGet()); + return count.get(); + } + + private long countFileSegment(MetadataStore metadataStore, String filePath) { + AtomicLong count = new AtomicLong(); + metadataStore.iterateFileSegment( + filePath, FileSegmentType.COMMIT_LOG, segmentMetadata -> count.incrementAndGet()); + return count.get(); + } + + @Test + public void testFileSegment() { + String filePath = MessageStoreUtil.toFilePath(mq0); + FileSegmentMetadata segmentMetadata1 = new FileSegmentMetadata( + filePath, 0L, FileSegmentType.COMMIT_LOG.getCode()); + metadataStore.updateFileSegment(segmentMetadata1); + Assert.assertEquals(1L, countFileSegment(metadataStore)); + + FileSegmentMetadata segmentMetadata2 = new FileSegmentMetadata( + filePath, 100, FileSegmentType.COMMIT_LOG.getCode()); + metadataStore.updateFileSegment(segmentMetadata2); + Assert.assertEquals(2L, countFileSegment(metadataStore)); + + FileSegmentMetadata segmentMetadata = metadataStore.getFileSegment( + filePath, FileSegmentType.COMMIT_LOG, 0L); + Assert.assertEquals(0L, segmentMetadata.getBaseOffset()); + Assert.assertEquals(0L, segmentMetadata.getSealTimestamp()); + Assert.assertEquals(FileSegmentMetadata.STATUS_NEW, segmentMetadata.getStatus()); + + segmentMetadata.markSealed(); + metadataStore.updateFileSegment(segmentMetadata); + segmentMetadata = metadataStore.getFileSegment( + filePath, FileSegmentType.COMMIT_LOG, segmentMetadata.getBaseOffset()); + Assert.assertEquals(FileSegmentMetadata.STATUS_SEALED, segmentMetadata.getStatus()); + Assert.assertNotEquals(0L, segmentMetadata.getSealTimestamp()); + + Assert.assertEquals(2L, countFileSegment(metadataStore, filePath)); + } + + @Test + public void testFileSegmentDelete() { + String filePath0 = MessageStoreUtil.toFilePath(mq0); + String filePath1 = MessageStoreUtil.toFilePath(mq1); + for (int i = 0; i < 10; i++) { + FileSegmentMetadata segmentMetadata = new FileSegmentMetadata( + filePath0, i * 1000L * 1000L, FileSegmentType.COMMIT_LOG.getCode()); + metadataStore.updateFileSegment(segmentMetadata); + + segmentMetadata = new FileSegmentMetadata( + filePath1, i * 1000L * 1000L, FileSegmentType.COMMIT_LOG.getCode()); + metadataStore.updateFileSegment(segmentMetadata); + } + Assert.assertEquals(20, countFileSegment(metadataStore)); + Assert.assertEquals(10, countFileSegment(metadataStore, filePath0)); + Assert.assertEquals(10, countFileSegment(metadataStore, filePath1)); + + metadataStore.deleteFileSegment(filePath0, FileSegmentType.COMMIT_LOG); + for (int i = 0; i < 5; i++) { + metadataStore.deleteFileSegment( + filePath1, FileSegmentType.COMMIT_LOG, i * 1000L * 1000L); + } + Assert.assertEquals(0L, countFileSegment(metadataStore, filePath0)); + Assert.assertEquals(5L, countFileSegment(metadataStore, filePath1)); + Assert.assertEquals(5L, countFileSegment(metadataStore)); + } + + @Test + public void testReload() { + DefaultMetadataStore defaultMetadataStore = (DefaultMetadataStore) metadataStore; + defaultMetadataStore.addTopic(mq0.getTopic(), 1); + defaultMetadataStore.addTopic(mq1.getTopic(), 2); + + defaultMetadataStore.addQueue(mq0, 2); + defaultMetadataStore.addQueue(mq1, 4); + defaultMetadataStore.addQueue(mq2, 8); + + String filePath0 = MessageStoreUtil.toFilePath(mq0); + FileSegmentMetadata segmentMetadata = + new FileSegmentMetadata(filePath0, 100, FileSegmentType.COMMIT_LOG.getCode()); + metadataStore.updateFileSegment(segmentMetadata); + segmentMetadata = + new FileSegmentMetadata(filePath0, 200, FileSegmentType.COMMIT_LOG.getCode()); + metadataStore.updateFileSegment(segmentMetadata); + + Assert.assertTrue(new File(defaultMetadataStore.configFilePath()).exists()); + + // Reload from disk + defaultMetadataStore = new DefaultMetadataStore(storeConfig); + defaultMetadataStore.load(); + TopicMetadata topicMetadata = defaultMetadataStore.getTopic(mq0.getTopic()); + Assert.assertNotNull(topicMetadata); + Assert.assertEquals(topicMetadata.getReserveTime(), 1); + + topicMetadata = defaultMetadataStore.getTopic(mq1.getTopic()); + Assert.assertNotNull(topicMetadata); + Assert.assertEquals(topicMetadata.getReserveTime(), 2); + + QueueMetadata queueMetadata = defaultMetadataStore.getQueue(mq0); + Assert.assertNotNull(queueMetadata); + Assert.assertEquals(mq0, queueMetadata.getQueue()); + Assert.assertEquals(queueMetadata.getMinOffset(), 2); + + queueMetadata = defaultMetadataStore.getQueue(mq1); + Assert.assertNotNull(queueMetadata); + Assert.assertEquals(mq1, queueMetadata.getQueue()); + Assert.assertEquals(queueMetadata.getMinOffset(), 4); + + queueMetadata = defaultMetadataStore.getQueue(mq2); + Assert.assertNotNull(queueMetadata); + Assert.assertEquals(mq2, queueMetadata.getQueue()); + Assert.assertEquals(queueMetadata.getMinOffset(), 8); + + Map map = new HashMap<>(); + defaultMetadataStore.iterateFileSegment(metadata -> map.put(metadata.getBaseOffset(), metadata)); + FileSegmentMetadata fileSegmentMetadata = map.get(100L); + Assert.assertNotNull(fileSegmentMetadata); + Assert.assertEquals(filePath0, fileSegmentMetadata.getPath()); + + fileSegmentMetadata = map.get(200L); + Assert.assertNotNull(fileSegmentMetadata); + Assert.assertEquals(filePath0, fileSegmentMetadata.getPath()); + } + + @Test + public void basicTest() { + this.testTopic(); + this.testQueue(); + this.testFileSegment(); + + ((DefaultMetadataStore) metadataStore).encode(); + ((DefaultMetadataStore) metadataStore).encode(false); + ((DefaultMetadataStore) metadataStore).encode(true); + } +} diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/metrics/TieredStoreMetricsManagerTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/metrics/TieredStoreMetricsManagerTest.java new file mode 100644 index 0000000..cc4d9e2 --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/metrics/TieredStoreMetricsManagerTest.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.tieredstore.metrics; + +import io.opentelemetry.sdk.OpenTelemetrySdk; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.TieredMessageStore; +import org.apache.rocketmq.tieredstore.core.MessageStoreFetcherImpl; +import org.apache.rocketmq.tieredstore.file.FlatFileStore; +import org.apache.rocketmq.tieredstore.provider.PosixFileSegment; +import org.junit.Test; +import org.mockito.Mockito; + +public class TieredStoreMetricsManagerTest { + + @Test + public void getMetricsView() { + TieredStoreMetricsManager.getMetricsView(); + } + + @Test + public void init() { + MessageStoreConfig storeConfig = new MessageStoreConfig(); + storeConfig.setTieredBackendServiceProvider(PosixFileSegment.class.getName()); + TieredMessageStore messageStore = Mockito.mock(TieredMessageStore.class); + Mockito.when(messageStore.getStoreConfig()).thenReturn(storeConfig); + Mockito.when(messageStore.getFlatFileStore()).thenReturn(Mockito.mock(FlatFileStore.class)); + MessageStoreFetcherImpl fetcher = Mockito.spy(new MessageStoreFetcherImpl(messageStore)); + + TieredStoreMetricsManager.init( + OpenTelemetrySdk.builder().build().getMeter(""), + null, storeConfig, fetcher, + Mockito.mock(FlatFileStore.class), Mockito.mock(DefaultMessageStore.class)); + } + + @Test + public void newAttributesBuilder() { + TieredStoreMetricsManager.newAttributesBuilder(); + } +} diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/provider/FileSegmentFactoryTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/provider/FileSegmentFactoryTest.java new file mode 100644 index 0000000..3b44b10 --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/provider/FileSegmentFactoryTest.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.tieredstore.provider; + +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.MessageStoreExecutor; +import org.apache.rocketmq.tieredstore.common.FileSegmentType; +import org.apache.rocketmq.tieredstore.metadata.DefaultMetadataStore; +import org.apache.rocketmq.tieredstore.metadata.MetadataStore; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtilTest; +import org.junit.Assert; +import org.junit.Test; + +public class FileSegmentFactoryTest { + + @Test + public void fileSegmentInstanceTest() throws ClassNotFoundException, NoSuchMethodException { + int baseOffset = 1000; + String filePath = "FileSegmentFactoryPath"; + String storePath = MessageStoreUtilTest.getRandomStorePath(); + MessageStoreConfig storeConfig = new MessageStoreConfig(); + storeConfig.setTieredStoreCommitLogMaxSize(1024); + storeConfig.setTieredStoreFilePath(storePath); + MessageStoreExecutor executor = new MessageStoreExecutor(); + + MetadataStore metadataStore = new DefaultMetadataStore(storeConfig); + FileSegmentFactory factory = new FileSegmentFactory(metadataStore, storeConfig, executor); + + Assert.assertEquals(metadataStore, factory.getMetadataStore()); + Assert.assertEquals(storeConfig, factory.getStoreConfig()); + + FileSegment fileSegment = factory.createCommitLogFileSegment(filePath, baseOffset); + Assert.assertEquals(1000, fileSegment.getBaseOffset()); + Assert.assertEquals(FileSegmentType.COMMIT_LOG, fileSegment.getFileType()); + fileSegment.destroyFile(); + + fileSegment = factory.createConsumeQueueFileSegment(filePath, baseOffset); + Assert.assertEquals(1000, fileSegment.getBaseOffset()); + Assert.assertEquals(FileSegmentType.CONSUME_QUEUE, fileSegment.getFileType()); + fileSegment.destroyFile(); + + fileSegment = factory.createIndexServiceFileSegment(filePath, baseOffset); + Assert.assertEquals(1000, fileSegment.getBaseOffset()); + Assert.assertEquals(FileSegmentType.INDEX, fileSegment.getFileType()); + fileSegment.destroyFile(); + + Assert.assertThrows(RuntimeException.class, + () -> factory.createSegment(null, null, 0L)); + storeConfig.setTieredBackendServiceProvider(null); + Assert.assertThrows(RuntimeException.class, + () -> new FileSegmentFactory(metadataStore, storeConfig, executor)); + + executor.shutdown(); + MessageStoreUtilTest.deleteStoreDirectory(storePath); + } +} diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/provider/FileSegmentTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/provider/FileSegmentTest.java new file mode 100644 index 0000000..2684411 --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/provider/FileSegmentTest.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.tieredstore.provider; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.MessageStoreExecutor; +import org.apache.rocketmq.tieredstore.common.AppendResult; +import org.apache.rocketmq.tieredstore.common.FileSegmentType; +import org.apache.rocketmq.tieredstore.exception.TieredStoreErrorCode; +import org.apache.rocketmq.tieredstore.exception.TieredStoreException; +import org.apache.rocketmq.tieredstore.metadata.DefaultMetadataStore; +import org.apache.rocketmq.tieredstore.metadata.MetadataStore; +import org.apache.rocketmq.tieredstore.util.MessageFormatUtil; +import org.apache.rocketmq.tieredstore.util.MessageFormatUtilTest; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtil; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtilTest; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; + +public class FileSegmentTest { + + public int baseOffset = 1000; + private final String storePath = MessageStoreUtilTest.getRandomStorePath(); + private MessageStoreConfig storeConfig; + private MessageQueue mq; + private MessageStoreExecutor storeExecutor; + + @Before + public void init() { + storeConfig = new MessageStoreConfig(); + storeConfig.setTieredStoreCommitLogMaxSize(2000); + storeConfig.setTieredStoreFilePath(storePath); + storeConfig.setTieredBackendServiceProvider(PosixFileSegment.class.getName()); + mq = new MessageQueue("FileSegmentTest", "brokerName", 0); + storeExecutor = new MessageStoreExecutor(); + } + + @After + public void shutdown() { + MessageStoreUtilTest.deleteStoreDirectory(storePath); + storeExecutor.shutdown(); + } + + @Test + public void fileAttributesTest() { + int baseOffset = 1000; + FileSegment fileSegment = new PosixFileSegment( + storeConfig, FileSegmentType.COMMIT_LOG, MessageStoreUtil.toFilePath(mq), baseOffset, storeExecutor); + + // for default value check + Assert.assertEquals(baseOffset, fileSegment.getBaseOffset()); + Assert.assertEquals(0L, fileSegment.getCommitPosition()); + Assert.assertEquals(0L, fileSegment.getAppendPosition()); + Assert.assertEquals(baseOffset, fileSegment.getCommitOffset()); + Assert.assertEquals(baseOffset, fileSegment.getAppendOffset()); + Assert.assertEquals(FileSegmentType.COMMIT_LOG, fileSegment.getFileType()); + Assert.assertEquals(Long.MAX_VALUE, fileSegment.getMinTimestamp()); + Assert.assertEquals(Long.MAX_VALUE, fileSegment.getMaxTimestamp()); + + // for recover + long timestamp = System.currentTimeMillis(); + fileSegment.setMinTimestamp(timestamp); + fileSegment.setMaxTimestamp(timestamp); + Assert.assertEquals(timestamp, fileSegment.getMinTimestamp()); + Assert.assertEquals(timestamp, fileSegment.getMaxTimestamp()); + + // for file status change + Assert.assertFalse(fileSegment.isClosed()); + fileSegment.close(); + Assert.assertTrue(fileSegment.isClosed()); + + fileSegment.destroyFile(); + } + + @Test + public void fileSortByOffsetTest() { + FileSegment fileSegment1 = new PosixFileSegment( + storeConfig, FileSegmentType.COMMIT_LOG, MessageStoreUtil.toFilePath(mq), 200L, storeExecutor); + FileSegment fileSegment2 = new PosixFileSegment( + storeConfig, FileSegmentType.COMMIT_LOG, MessageStoreUtil.toFilePath(mq), 100L, storeExecutor); + FileSegment[] fileSegments = new FileSegment[] {fileSegment1, fileSegment2}; + Arrays.sort(fileSegments); + Assert.assertEquals(fileSegments[0], fileSegment2); + Assert.assertEquals(fileSegments[1], fileSegment1); + } + + @Test + public void fileMaxSizeTest() { + FileSegment fileSegment = new PosixFileSegment( + storeConfig, FileSegmentType.COMMIT_LOG, MessageStoreUtil.toFilePath(mq), 100L, storeExecutor); + Assert.assertEquals(storeConfig.getTieredStoreCommitLogMaxSize(), fileSegment.getMaxSize()); + fileSegment.destroyFile(); + + fileSegment = new PosixFileSegment( + storeConfig, FileSegmentType.CONSUME_QUEUE, MessageStoreUtil.toFilePath(mq), 100L, storeExecutor); + Assert.assertEquals(storeConfig.getTieredStoreConsumeQueueMaxSize(), fileSegment.getMaxSize()); + fileSegment.destroyFile(); + + fileSegment = new PosixFileSegment( + storeConfig, FileSegmentType.INDEX, MessageStoreUtil.toFilePath(mq), 100L, storeExecutor); + Assert.assertEquals(Long.MAX_VALUE, fileSegment.getMaxSize()); + fileSegment.destroyFile(); + } + + @Test + public void unexpectedCaseTest() { + MetadataStore metadataStore = new DefaultMetadataStore(storeConfig); + FileSegmentFactory factory = new FileSegmentFactory(metadataStore, storeConfig, new MessageStoreExecutor()); + FileSegment fileSegment = factory.createCommitLogFileSegment(MessageStoreUtil.toFilePath(mq), baseOffset); + + fileSegment.initPosition(fileSegment.getSize()); + Assert.assertFalse(fileSegment.needCommit()); + Assert.assertTrue(fileSegment.commitAsync().join()); + + fileSegment.append(ByteBuffer.allocate(0), 0L); + Assert.assertTrue(fileSegment.commitAsync().join()); + + ByteBuffer byteBuffer = ByteBuffer.allocate(8); + byteBuffer.putLong(0L); + byteBuffer.flip(); + fileSegment.append(byteBuffer, 0L); + + byteBuffer.getLong(); + Assert.assertTrue(fileSegment.commitAsync().join()); + fileSegment.destroyFile(); + } + + @Test + public void commitLogTest() throws InterruptedException { + MetadataStore metadataStore = new DefaultMetadataStore(storeConfig); + FileSegmentFactory factory = new FileSegmentFactory(metadataStore, storeConfig, new MessageStoreExecutor()); + FileSegment fileSegment = factory.createCommitLogFileSegment(MessageStoreUtil.toFilePath(mq), baseOffset); + long lastSize = fileSegment.getSize(); + fileSegment.initPosition(fileSegment.getSize()); + Assert.assertFalse(fileSegment.needCommit()); + Assert.assertTrue(fileSegment.commitAsync().join()); + + fileSegment.append(MessageFormatUtilTest.buildMockedMessageBuffer(), 0L); + fileSegment.append(MessageFormatUtilTest.buildMockedMessageBuffer(), 0L); + Assert.assertTrue(fileSegment.needCommit()); + + fileSegment.commitLock.acquire(); + Assert.assertFalse(fileSegment.commitAsync().join()); + fileSegment.commitLock.release(); + + long storeTimestamp = System.currentTimeMillis(); + ByteBuffer buffer = MessageFormatUtilTest.buildMockedMessageBuffer(); + buffer.putLong(MessageFormatUtil.STORE_TIMESTAMP_POSITION, storeTimestamp); + buffer.putLong(MessageFormatUtil.QUEUE_OFFSET_POSITION, 100L); + fileSegment.append(buffer, storeTimestamp); + + Assert.assertTrue(fileSegment.needCommit()); + Assert.assertEquals(baseOffset, fileSegment.getBaseOffset()); + Assert.assertEquals(baseOffset + lastSize + MessageFormatUtilTest.MSG_LEN * 3, fileSegment.getAppendOffset()); + Assert.assertEquals(0L, fileSegment.getMinTimestamp()); + Assert.assertEquals(storeTimestamp, fileSegment.getMaxTimestamp()); + + List buffers = fileSegment.borrowBuffer(); + Assert.assertEquals(3, buffers.size()); + fileSegment.bufferList.addAll(buffers); + + fileSegment.commitAsync().join(); + Assert.assertFalse(fileSegment.needCommit()); + Assert.assertEquals(fileSegment.getCommitOffset(), fileSegment.getAppendOffset()); + + // offset will change when type is commitLog + ByteBuffer msg1 = fileSegment.read(lastSize, MessageFormatUtilTest.MSG_LEN); + Assert.assertEquals(baseOffset + lastSize, MessageFormatUtil.getCommitLogOffset(msg1)); + + ByteBuffer msg2 = fileSegment.read(lastSize + MessageFormatUtilTest.MSG_LEN, MessageFormatUtilTest.MSG_LEN); + Assert.assertEquals(baseOffset + lastSize + MessageFormatUtilTest.MSG_LEN, MessageFormatUtil.getCommitLogOffset(msg2)); + + ByteBuffer msg3 = fileSegment.read(lastSize + MessageFormatUtilTest.MSG_LEN * 2, MessageFormatUtilTest.MSG_LEN); + Assert.assertEquals(baseOffset + lastSize + MessageFormatUtilTest.MSG_LEN * 2, MessageFormatUtil.getCommitLogOffset(msg3)); + + // buffer full + fileSegment.bufferList.addAll(buffers); + storeConfig.setTieredStoreMaxGroupCommitCount(3); + Assert.assertEquals(AppendResult.BUFFER_FULL, + fileSegment.append(MessageFormatUtilTest.buildMockedMessageBuffer(), 0L)); + + // file full + fileSegment.initPosition(storeConfig.getTieredStoreCommitLogMaxSize() - MessageFormatUtilTest.MSG_LEN + 1); + Assert.assertEquals(AppendResult.FILE_FULL, + fileSegment.append(MessageFormatUtilTest.buildMockedMessageBuffer(), 0L)); + + // file close + fileSegment.close(); + Assert.assertEquals(AppendResult.FILE_CLOSED, + fileSegment.append(MessageFormatUtilTest.buildMockedMessageBuffer(), 0L)); + Assert.assertFalse(fileSegment.commitAsync().join()); + + fileSegment.destroyFile(); + } + + @Test + public void consumeQueueTest() throws ClassNotFoundException, NoSuchMethodException { + MetadataStore metadataStore = new DefaultMetadataStore(storeConfig); + FileSegmentFactory factory = new FileSegmentFactory(metadataStore, storeConfig, new MessageStoreExecutor()); + FileSegment fileSegment = factory.createConsumeQueueFileSegment(MessageStoreUtil.toFilePath(mq), baseOffset); + + long storeTimestamp = System.currentTimeMillis(); + int messageSize = MessageFormatUtilTest.MSG_LEN; + int unitSize = MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE; + long initPosition = 5 * unitSize; + + fileSegment.initPosition(initPosition); + fileSegment.append(MessageFormatUtilTest.buildMockedConsumeQueueBuffer().putLong(0, baseOffset), 0); + fileSegment.append(MessageFormatUtilTest.buildMockedConsumeQueueBuffer().putLong(0, baseOffset + messageSize), 0); + fileSegment.append(MessageFormatUtilTest.buildMockedConsumeQueueBuffer().putLong(0, baseOffset + messageSize * 2), storeTimestamp); + + Assert.assertEquals(initPosition + unitSize * 3, fileSegment.getAppendPosition()); + Assert.assertEquals(0, fileSegment.getMinTimestamp()); + Assert.assertEquals(storeTimestamp, fileSegment.getMaxTimestamp()); + + fileSegment.commitAsync().join(); + Assert.assertEquals(fileSegment.getAppendOffset(), fileSegment.getCommitOffset()); + + ByteBuffer cqItem1 = fileSegment.read(initPosition, unitSize); + Assert.assertEquals(baseOffset, cqItem1.getLong()); + + ByteBuffer cqItem2 = fileSegment.read(initPosition + unitSize, unitSize); + Assert.assertEquals(baseOffset + messageSize, cqItem2.getLong()); + + ByteBuffer cqItem3 = fileSegment.read(initPosition + unitSize * 2, unitSize); + Assert.assertEquals(baseOffset + messageSize * 2, cqItem3.getLong()); + } + + @Test + public void fileSegmentReadTest() throws ClassNotFoundException, NoSuchMethodException { + MetadataStore metadataStore = new DefaultMetadataStore(storeConfig); + FileSegmentFactory factory = new FileSegmentFactory(metadataStore, storeConfig, new MessageStoreExecutor()); + FileSegment fileSegment = factory.createConsumeQueueFileSegment(MessageStoreUtil.toFilePath(mq), baseOffset); + + long storeTimestamp = System.currentTimeMillis(); + int messageSize = MessageFormatUtilTest.MSG_LEN; + int unitSize = MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE; + long initPosition = 5 * unitSize; + + fileSegment.initPosition(initPosition); + fileSegment.append(MessageFormatUtilTest.buildMockedConsumeQueueBuffer().putLong(0, baseOffset), 0); + fileSegment.append(MessageFormatUtilTest.buildMockedConsumeQueueBuffer().putLong(0, baseOffset + messageSize), 0); + fileSegment.append(MessageFormatUtilTest.buildMockedConsumeQueueBuffer().putLong(0, baseOffset + messageSize * 2), storeTimestamp); + fileSegment.commitAsync().join(); + + CompletionException exception = Assert.assertThrows( + CompletionException.class, () -> fileSegment.read(-1, -1)); + Assert.assertTrue(exception.getCause() instanceof TieredStoreException); + Assert.assertEquals(TieredStoreErrorCode.ILLEGAL_PARAM, ((TieredStoreException) exception.getCause()).getErrorCode()); + + exception = Assert.assertThrows( + CompletionException.class, () -> fileSegment.read(100, 0)); + Assert.assertTrue(exception.getCause() instanceof TieredStoreException); + Assert.assertEquals(TieredStoreErrorCode.ILLEGAL_PARAM, ((TieredStoreException) exception.getCause()).getErrorCode()); + + // at most three messages + Assert.assertEquals(unitSize * 3, + fileSegment.read(100, messageSize * 3).remaining()); + Assert.assertEquals(unitSize * 3, + fileSegment.read(100, messageSize * 5).remaining()); + } + + @Test + public void commitFailedThenSuccessTest() { + MemoryFileSegment segment = new MemoryFileSegment( + storeConfig, FileSegmentType.COMMIT_LOG, MessageStoreUtil.toFilePath(mq), baseOffset, storeExecutor); + + long lastSize = segment.getSize(); + segment.setCheckSize(false); + segment.initPosition(lastSize); + segment.setSize((int) lastSize); + + int messageSize = MessageFormatUtilTest.MSG_LEN; + ByteBuffer buffer1 = MessageFormatUtilTest.buildMockedMessageBuffer().putLong( + MessageFormatUtil.PHYSICAL_OFFSET_POSITION, baseOffset + lastSize); + ByteBuffer buffer2 = MessageFormatUtilTest.buildMockedMessageBuffer().putLong( + MessageFormatUtil.PHYSICAL_OFFSET_POSITION, baseOffset + lastSize + messageSize); + Assert.assertEquals(AppendResult.SUCCESS, segment.append(buffer1, 0)); + Assert.assertEquals(AppendResult.SUCCESS, segment.append(buffer2, 0)); + + // Mock new message arrive + long timestamp = System.currentTimeMillis(); + segment.blocker = new CompletableFuture<>(); + new Thread(() -> { + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + Assert.fail(e.getMessage()); + } + ByteBuffer buffer = MessageFormatUtilTest.buildMockedMessageBuffer(); + buffer.putLong(MessageFormatUtil.PHYSICAL_OFFSET_POSITION, messageSize * 2); + buffer.putLong(MessageFormatUtil.STORE_TIMESTAMP_POSITION, timestamp); + segment.append(buffer, 0); + segment.blocker.complete(false); + }).start(); + + // Commit failed + segment.commitAsync().join(); + segment.blocker.join(); + segment.blocker = null; + + // Copy data and assume commit success + segment.getMemStore().put(buffer1); + segment.getMemStore().put(buffer2); + segment.setSize((int) (lastSize + messageSize * 2)); + + segment.commitAsync().join(); + Assert.assertEquals(lastSize + messageSize * 3, segment.getCommitPosition()); + Assert.assertEquals(baseOffset + lastSize + messageSize * 3, segment.getCommitOffset()); + Assert.assertEquals(baseOffset + lastSize + messageSize * 3, segment.getAppendOffset()); + + ByteBuffer msg1 = segment.read(lastSize, messageSize); + Assert.assertEquals(baseOffset + lastSize, MessageFormatUtil.getCommitLogOffset(msg1)); + + ByteBuffer msg2 = segment.read(lastSize + messageSize, messageSize); + Assert.assertEquals(baseOffset + lastSize + messageSize, MessageFormatUtil.getCommitLogOffset(msg2)); + + ByteBuffer msg3 = segment.read(lastSize + messageSize * 2, messageSize); + Assert.assertEquals(baseOffset + lastSize + messageSize * 2, MessageFormatUtil.getCommitLogOffset(msg3)); + } + + @Test + public void commitFailedMoreTimes() { + long startTime = System.currentTimeMillis(); + MemoryFileSegment segment = new MemoryFileSegment( + storeConfig, FileSegmentType.COMMIT_LOG, MessageStoreUtil.toFilePath(mq), baseOffset, storeExecutor); + + long lastSize = segment.getSize(); + segment.setCheckSize(false); + segment.initPosition(lastSize); + segment.setSize((int) lastSize); + + ByteBuffer buffer1 = MessageFormatUtilTest.buildMockedMessageBuffer().putLong( + MessageFormatUtil.PHYSICAL_OFFSET_POSITION, baseOffset + lastSize); + ByteBuffer buffer2 = MessageFormatUtilTest.buildMockedMessageBuffer().putLong( + MessageFormatUtil.PHYSICAL_OFFSET_POSITION, baseOffset + lastSize + MessageFormatUtilTest.MSG_LEN); + segment.append(buffer1, 0); + segment.append(buffer2, 0); + + // Mock new message arrive + segment.blocker = new CompletableFuture<>(); + new Thread(() -> { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Assert.fail(e.getMessage()); + } + ByteBuffer buffer = MessageFormatUtilTest.buildMockedMessageBuffer(); + buffer.putLong(MessageFormatUtil.PHYSICAL_OFFSET_POSITION, MessageFormatUtilTest.MSG_LEN * 2); + buffer.putLong(MessageFormatUtil.STORE_TIMESTAMP_POSITION, startTime); + segment.append(buffer, 0); + segment.blocker.complete(false); + }).start(); + + for (int i = 0; i < 3; i++) { + Assert.assertFalse(segment.commitAsync().join()); + } + + FileSegment fileSpySegment = Mockito.spy(segment); + Mockito.when(fileSpySegment.getSize()).thenReturn(-1L); + Assert.assertFalse(fileSpySegment.commitAsync().join()); + + Assert.assertEquals(lastSize, segment.getCommitPosition()); + Assert.assertEquals(baseOffset + lastSize, segment.getCommitOffset()); + Assert.assertEquals(baseOffset + lastSize + MessageFormatUtilTest.MSG_LEN * 3, segment.getAppendOffset()); + + segment.blocker.join(); + segment.blocker = null; + + segment.commitAsync().join(); + Assert.assertEquals(lastSize + MessageFormatUtilTest.MSG_LEN * 2, segment.getCommitPosition()); + Assert.assertEquals(baseOffset + lastSize + MessageFormatUtilTest.MSG_LEN * 2, segment.getCommitOffset()); + Assert.assertEquals(baseOffset + lastSize + MessageFormatUtilTest.MSG_LEN * 3, segment.getAppendOffset()); + + segment.commitAsync().join(); + Assert.assertEquals(lastSize + MessageFormatUtilTest.MSG_LEN * 3, segment.getCommitPosition()); + Assert.assertEquals(baseOffset + lastSize + MessageFormatUtilTest.MSG_LEN * 3, segment.getCommitOffset()); + Assert.assertEquals(baseOffset + lastSize + MessageFormatUtilTest.MSG_LEN * 3, segment.getAppendOffset()); + + ByteBuffer msg1 = segment.read(lastSize, MessageFormatUtilTest.MSG_LEN); + Assert.assertEquals(baseOffset + lastSize, MessageFormatUtil.getCommitLogOffset(msg1)); + + ByteBuffer msg2 = segment.read(lastSize + MessageFormatUtilTest.MSG_LEN, MessageFormatUtilTest.MSG_LEN); + Assert.assertEquals(baseOffset + lastSize + MessageFormatUtilTest.MSG_LEN, MessageFormatUtil.getCommitLogOffset(msg2)); + + ByteBuffer msg3 = segment.read(lastSize + MessageFormatUtilTest.MSG_LEN * 2, MessageFormatUtilTest.MSG_LEN); + Assert.assertEquals(baseOffset + lastSize + MessageFormatUtilTest.MSG_LEN * 2, MessageFormatUtil.getCommitLogOffset(msg3)); + } + + @Test + public void handleCommitExceptionTest() { + MetadataStore metadataStore = new DefaultMetadataStore(storeConfig); + FileSegmentFactory factory = new FileSegmentFactory(metadataStore, storeConfig, storeExecutor); + + { + FileSegment fileSegment = factory.createCommitLogFileSegment(MessageStoreUtil.toFilePath(mq), baseOffset); + FileSegment fileSpySegment = Mockito.spy(fileSegment); + fileSpySegment.append(MessageFormatUtilTest.buildMockedMessageBuffer(), 0L); + fileSpySegment.append(MessageFormatUtilTest.buildMockedMessageBuffer(), 0L); + + Mockito.when(fileSpySegment.commit0(any(), anyLong(), anyInt(), anyBoolean())) + .thenReturn(CompletableFuture.supplyAsync(() -> { + throw new TieredStoreException(TieredStoreErrorCode.IO_ERROR, "Test"); + })); + Assert.assertFalse(fileSpySegment.commitAsync().join()); + fileSegment.destroyFile(); + } + + { + FileSegment fileSegment = factory.createCommitLogFileSegment(MessageStoreUtil.toFilePath(mq), baseOffset); + FileSegment fileSpySegment = Mockito.spy(fileSegment); + fileSpySegment.append(MessageFormatUtilTest.buildMockedMessageBuffer(), 0L); + fileSpySegment.append(MessageFormatUtilTest.buildMockedMessageBuffer(), 0L); + + Mockito.when(fileSpySegment.commit0(any(), anyLong(), anyInt(), anyBoolean())) + .thenReturn(CompletableFuture.supplyAsync(() -> { + long size = MessageFormatUtilTest.buildMockedMessageBuffer().remaining(); + TieredStoreException exception = new TieredStoreException(TieredStoreErrorCode.IO_ERROR, "Test"); + exception.setPosition(size * 2L); + throw exception; + })); + Assert.assertTrue(fileSpySegment.commitAsync().join()); + fileSegment.destroyFile(); + } + + { + FileSegment fileSegment = factory.createCommitLogFileSegment(MessageStoreUtil.toFilePath(mq), baseOffset); + FileSegment fileSpySegment = Mockito.spy(fileSegment); + fileSpySegment.append(MessageFormatUtilTest.buildMockedMessageBuffer(), 0L); + fileSpySegment.append(MessageFormatUtilTest.buildMockedMessageBuffer(), 0L); + + Mockito.when(fileSpySegment.commit0(any(), anyLong(), anyInt(), anyBoolean())) + .thenReturn(CompletableFuture.supplyAsync(() -> { + throw new RuntimeException("Runtime Error for Test"); + })); + Mockito.when(fileSpySegment.getSize()).thenReturn(0L); + Assert.assertFalse(fileSpySegment.commitAsync().join()); + } + } +} diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/provider/MemoryFileSegmentTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/provider/MemoryFileSegmentTest.java new file mode 100644 index 0000000..7239650 --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/provider/MemoryFileSegmentTest.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.tieredstore.provider; + +import java.io.IOException; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.tieredstore.MessageStoreConfig; +import org.apache.rocketmq.tieredstore.MessageStoreExecutor; +import org.apache.rocketmq.tieredstore.common.FileSegmentType; +import org.apache.rocketmq.tieredstore.stream.FileSegmentInputStream; +import org.apache.rocketmq.tieredstore.util.MessageStoreUtil; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import static org.mockito.ArgumentMatchers.any; + +public class MemoryFileSegmentTest { + + @Test + public void memoryTest() throws IOException { + MemoryFileSegment fileSegment = new MemoryFileSegment( + new MessageStoreConfig(), FileSegmentType.COMMIT_LOG, + MessageStoreUtil.toFilePath(new MessageQueue()), 0L, new MessageStoreExecutor()); + Assert.assertFalse(fileSegment.exists()); + fileSegment.createFile(); + MemoryFileSegment fileSpySegment = Mockito.spy(fileSegment); + FileSegmentInputStream inputStream = Mockito.mock(FileSegmentInputStream.class); + Mockito.when(inputStream.read(any())).thenThrow(new RuntimeException()); + Assert.assertFalse(fileSpySegment.commit0(inputStream, 0L, 0, false).join()); + fileSegment.destroyFile(); + } +} \ No newline at end of file diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/stream/FileSegmentInputStreamTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/stream/FileSegmentInputStreamTest.java new file mode 100644 index 0000000..3d0dd57 --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/stream/FileSegmentInputStreamTest.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.tieredstore.stream; + +import com.google.common.base.Supplier; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import org.apache.rocketmq.tieredstore.common.FileSegmentType; +import org.apache.rocketmq.tieredstore.util.MessageFormatUtil; +import org.apache.rocketmq.tieredstore.util.MessageFormatUtilTest; +import org.junit.Assert; +import org.junit.Test; + +public class FileSegmentInputStreamTest { + + private final static long COMMIT_LOG_START_OFFSET = 13131313; + + private final static int MSG_LEN = MessageFormatUtilTest.MSG_LEN; + + private final static int MSG_NUM = 10; + + private final static int RESET_TIMES = 10; + + private final static Random RANDOM = new Random(); + + @Test + public void testCommitLogTypeInputStream() { + List uploadBufferList = new ArrayList<>(); + int bufferSize = 0; + for (int i = 0; i < MSG_NUM; i++) { + ByteBuffer byteBuffer = MessageFormatUtilTest.buildMockedMessageBuffer(); + uploadBufferList.add(byteBuffer); + bufferSize += byteBuffer.remaining(); + } + + // build expected byte buffer for verifying the FileSegmentInputStream + ByteBuffer expectedByteBuffer = ByteBuffer.allocate(bufferSize); + for (ByteBuffer byteBuffer : uploadBufferList) { + expectedByteBuffer.put(byteBuffer); + byteBuffer.rewind(); + } + // set real physical offset + for (int i = 0; i < MSG_NUM; i++) { + long physicalOffset = COMMIT_LOG_START_OFFSET + i * MSG_LEN; + int position = i * MSG_LEN + MessageFormatUtil.PHYSICAL_OFFSET_POSITION; + expectedByteBuffer.putLong(position, physicalOffset); + } + + int finalBufferSize = bufferSize; + int[] batchReadSizeTestSet = { + MessageFormatUtil.PHYSICAL_OFFSET_POSITION - 1, MessageFormatUtil.PHYSICAL_OFFSET_POSITION, MessageFormatUtil.PHYSICAL_OFFSET_POSITION + 1, MSG_LEN - 1, MSG_LEN, MSG_LEN + 1 + }; + verifyReadAndReset(expectedByteBuffer, () -> FileSegmentInputStreamFactory.build( + FileSegmentType.COMMIT_LOG, COMMIT_LOG_START_OFFSET, uploadBufferList, null, finalBufferSize), finalBufferSize, batchReadSizeTestSet); + + } + + @Test + public void testCommitLogTypeInputStreamWithCoda() { + List uploadBufferList = new ArrayList<>(); + int bufferSize = 0; + for (int i = 0; i < MSG_NUM; i++) { + ByteBuffer byteBuffer = MessageFormatUtilTest.buildMockedMessageBuffer(); + uploadBufferList.add(byteBuffer); + bufferSize += byteBuffer.remaining(); + } + + ByteBuffer codaBuffer = ByteBuffer.allocate(MessageFormatUtil.COMMIT_LOG_CODA_SIZE); + codaBuffer.putInt(MessageFormatUtil.COMMIT_LOG_CODA_SIZE); + codaBuffer.putInt(MessageFormatUtil.BLANK_MAGIC_CODE); + long timeMillis = System.currentTimeMillis(); + codaBuffer.putLong(timeMillis); + codaBuffer.flip(); + int codaBufferSize = codaBuffer.remaining(); + bufferSize += codaBufferSize; + + // build expected byte buffer for verifying the FileSegmentInputStream + ByteBuffer expectedByteBuffer = ByteBuffer.allocate(bufferSize); + for (ByteBuffer byteBuffer : uploadBufferList) { + expectedByteBuffer.put(byteBuffer); + byteBuffer.rewind(); + } + expectedByteBuffer.put(codaBuffer); + codaBuffer.rewind(); + // set real physical offset + for (int i = 0; i < MSG_NUM; i++) { + long physicalOffset = COMMIT_LOG_START_OFFSET + i * MSG_LEN; + int position = i * MSG_LEN + MessageFormatUtil.PHYSICAL_OFFSET_POSITION; + expectedByteBuffer.putLong(position, physicalOffset); + } + + int finalBufferSize = bufferSize; + int[] batchReadSizeTestSet = { + MessageFormatUtil.PHYSICAL_OFFSET_POSITION - 1, MessageFormatUtil.PHYSICAL_OFFSET_POSITION, MessageFormatUtil.PHYSICAL_OFFSET_POSITION + 1, + MSG_LEN - 1, MSG_LEN, MSG_LEN + 1, + bufferSize - 1, bufferSize, bufferSize + 1 + }; + verifyReadAndReset(expectedByteBuffer, () -> FileSegmentInputStreamFactory.build( + FileSegmentType.COMMIT_LOG, COMMIT_LOG_START_OFFSET, uploadBufferList, codaBuffer, finalBufferSize), finalBufferSize, batchReadSizeTestSet); + + } + + @Test + public void testConsumeQueueTypeInputStream() { + List uploadBufferList = new ArrayList<>(); + int bufferSize = 0; + for (int i = 0; i < MSG_NUM; i++) { + ByteBuffer byteBuffer = MessageFormatUtilTest.buildMockedConsumeQueueBuffer(); + uploadBufferList.add(byteBuffer); + bufferSize += byteBuffer.remaining(); + } + + // build expected byte buffer for verifying the FileSegmentInputStream + ByteBuffer expectedByteBuffer = ByteBuffer.allocate(bufferSize); + for (ByteBuffer byteBuffer : uploadBufferList) { + expectedByteBuffer.put(byteBuffer); + byteBuffer.rewind(); + } + + int finalBufferSize = bufferSize; + int[] batchReadSizeTestSet = {MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE - 1, MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE, MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE + 1}; + verifyReadAndReset(expectedByteBuffer, () -> FileSegmentInputStreamFactory.build( + FileSegmentType.CONSUME_QUEUE, COMMIT_LOG_START_OFFSET, uploadBufferList, null, finalBufferSize), bufferSize, batchReadSizeTestSet); + } + + @Test + public void testIndexTypeInputStream() { + ByteBuffer byteBuffer = ByteBuffer.allocate(24); + byteBuffer.putLong(1); + byteBuffer.putLong(2); + byteBuffer.putLong(3); + byteBuffer.flip(); + List uploadBufferList = Arrays.asList(byteBuffer); + + // build expected byte buffer for verifying the FileSegmentInputStream + ByteBuffer expectedByteBuffer = byteBuffer.slice(); + + verifyReadAndReset(expectedByteBuffer, () -> FileSegmentInputStreamFactory.build( + FileSegmentType.INDEX, COMMIT_LOG_START_OFFSET, uploadBufferList, null, byteBuffer.limit()), byteBuffer.limit(), new int[] {23, 24, 25}); + } + + private void verifyReadAndReset(ByteBuffer expectedByteBuffer, Supplier constructor, + int bufferSize, int[] readBatchSizeTestSet) { + FileSegmentInputStream inputStream = constructor.get(); + + // verify + verifyInputStream(inputStream, expectedByteBuffer); + + // verify reset with method InputStream#mark() hasn't been called + try { + inputStream.reset(); + Assert.fail("Should throw IOException"); + } catch (IOException e) { + Assert.assertTrue(e instanceof IOException); + } + + // verify reset with method InputStream#mark() has been called + int resetPosition = RANDOM.nextInt(bufferSize); + int expectedResetPosition = 0; + inputStream = constructor.get(); + // verify and mark with resetPosition, use read() to read a byte each time + for (int i = 0; i < RESET_TIMES; i++) { + verifyInputStream(inputStream, expectedByteBuffer, expectedResetPosition, resetPosition); + + try { + inputStream.reset(); + } catch (IOException e) { + Assert.fail("Should not throw IOException"); + } + + expectedResetPosition = resetPosition; + resetPosition += RANDOM.nextInt(bufferSize - resetPosition); + } + for (int i = 0; i < readBatchSizeTestSet.length; i++) { + inputStream = constructor.get(); + int readBatchSize = readBatchSizeTestSet[i]; + expectedResetPosition = 0; + resetPosition = readBatchSize * RANDOM.nextInt(1 + bufferSize / readBatchSize); + // verify and mark with resetPosition, use read(byte[]) to read a byte array each time + for (int j = 0; j < RESET_TIMES; j++) { + verifyInputStreamViaBatchRead(inputStream, expectedByteBuffer, expectedResetPosition, resetPosition, readBatchSize); + try { + inputStream.reset(); + } catch (IOException e) { + Assert.fail("Should not throw IOException"); + } + + expectedResetPosition = resetPosition; + resetPosition += readBatchSize * RANDOM.nextInt(1 + (bufferSize - resetPosition) / readBatchSize); + } + } + } + + private void verifyInputStream(InputStream inputStream, ByteBuffer expectedBuffer) { + verifyInputStream(inputStream, expectedBuffer, 0, -1); + } + + /** + * verify the input stream + * + * @param inputStream the input stream to be verified + * @param expectedBuffer the expected byte buffer + * @param expectedBufferReadPos the expected start position of the expected byte buffer + * @param expectedMarkCalledPos the expected position when the method InputStream#mark() is called. (-1 means ignored) + */ + private void verifyInputStream(InputStream inputStream, ByteBuffer expectedBuffer, int expectedBufferReadPos, + int expectedMarkCalledPos) { + try { + expectedBuffer.position(expectedBufferReadPos); + while (true) { + if (expectedMarkCalledPos == expectedBuffer.position()) { + inputStream.mark(0); + } + int b = inputStream.read(); + if (b == -1) + break; + Assert.assertEquals(expectedBuffer.get(), (byte) b); + } + Assert.assertFalse(expectedBuffer.hasRemaining()); + } catch (IOException e) { + Assert.fail(e.getMessage()); + } + } + + /** + * verify the input stream + * + * @param inputStream the input stream to be verified + * @param expectedBuffer the expected byte buffer + * @param expectedBufferReadPos the expected start position of the expected byte buffer + * @param expectedMarkCalledPos the expected position when the method InputStream#mark() is called. (-1 means ignored) + * @param readBatchSize the batch size of each read(byte[]) operation + */ + private void verifyInputStreamViaBatchRead(InputStream inputStream, ByteBuffer expectedBuffer, + int expectedBufferReadPos, int expectedMarkCalledPos, int readBatchSize) { + try { + expectedBuffer.position(expectedBufferReadPos); + byte[] buf = new byte[readBatchSize]; + while (true) { + if (expectedMarkCalledPos == expectedBuffer.position()) { + inputStream.mark(0); + } + int len = inputStream.read(buf, 0, readBatchSize); + if (len == -1) + break; + byte[] expected = new byte[len]; + expectedBuffer.get(expected, 0, len); + for (int i = 0; i < len; i++) { + Assert.assertEquals(expected[i], buf[i]); + } + } + Assert.assertFalse(expectedBuffer.hasRemaining()); + } catch (IOException e) { + Assert.fail(e.getMessage()); + } + } + +} diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/util/MessageFormatUtilTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/util/MessageFormatUtilTest.java new file mode 100644 index 0000000..be4805b --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/util/MessageFormatUtilTest.java @@ -0,0 +1,266 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tieredstore.util; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.tieredstore.common.SelectBufferResult; +import org.junit.Assert; +import org.junit.Test; + +import static org.apache.rocketmq.tieredstore.util.MessageFormatUtil.COMMIT_LOG_CODA_SIZE; + +public class MessageFormatUtilTest { + + public static final int MSG_LEN = 123; + + public static ByteBuffer buildMockedMessageBuffer() { + ByteBuffer buffer = ByteBuffer.allocate(MSG_LEN); + buffer.putInt(MSG_LEN); + buffer.putInt(MessageDecoder.MESSAGE_MAGIC_CODE_V2); + buffer.putInt(3); + buffer.putInt(4); + buffer.putInt(5); + buffer.putLong(6); + buffer.putLong(7); + buffer.putInt(8); + buffer.putLong(9); + buffer.putLong(10); + buffer.putLong(11); + buffer.putLong(10); + buffer.putInt(13); + buffer.putLong(14); + buffer.putInt(0); + buffer.putShort((short) 0); + + Map map = new HashMap<>(); + map.put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, "uk"); + map.put("UserKey", "UserValue0"); + String properties = MessageDecoder.messageProperties2String(map); + byte[] propertiesBytes = properties.getBytes(StandardCharsets.UTF_8); + buffer.putShort((short) propertiesBytes.length); + buffer.put(propertiesBytes); + buffer.flip(); + + Assert.assertEquals(MSG_LEN, buffer.remaining()); + return buffer; + } + + @Test + public void verifyMockedMessageBuffer() { + ByteBuffer buffer = buildMockedMessageBuffer(); + Assert.assertEquals(MSG_LEN, buffer.remaining()); + Assert.assertEquals(MSG_LEN, buffer.getInt()); + Assert.assertEquals(MessageDecoder.MESSAGE_MAGIC_CODE_V2, buffer.getInt()); + Assert.assertEquals(3, buffer.getInt()); + Assert.assertEquals(4, buffer.getInt()); + Assert.assertEquals(5, buffer.getInt()); + Assert.assertEquals(6, buffer.getLong()); + Assert.assertEquals(7, buffer.getLong()); + Assert.assertEquals(8, buffer.getInt()); + Assert.assertEquals(9, buffer.getLong()); + Assert.assertEquals(10, buffer.getLong()); + Assert.assertEquals(11, buffer.getLong()); + Assert.assertEquals(10, buffer.getLong()); + Assert.assertEquals(13, buffer.getInt()); + Assert.assertEquals(14, buffer.getLong()); + Assert.assertEquals(0, buffer.getInt()); + Assert.assertEquals(0, buffer.getShort()); + buffer.rewind(); + Map properties = MessageFormatUtil.getProperties(buffer); + Assert.assertEquals("uk", properties.get(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX)); + Assert.assertEquals("UserValue0", properties.get("UserKey")); + } + + public static ByteBuffer buildMockedConsumeQueueBuffer() { + ByteBuffer buffer = ByteBuffer.allocate(MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE); + buffer.putLong(1L); + buffer.putInt(2); + buffer.putLong(3L); + buffer.flip(); + return buffer; + } + + @Test + public void verifyMockedConsumeQueueBuffer() { + ByteBuffer buffer = buildMockedConsumeQueueBuffer(); + Assert.assertEquals(1L, MessageFormatUtil.getCommitLogOffsetFromItem(buffer)); + Assert.assertEquals(2, MessageFormatUtil.getSizeFromItem(buffer)); + Assert.assertEquals(3L, MessageFormatUtil.getTagCodeFromItem(buffer)); + } + + @Test + public void messageFormatBasicTest() { + ByteBuffer buffer = buildMockedMessageBuffer(); + Assert.assertEquals(MSG_LEN, MessageFormatUtil.getTotalSize(buffer)); + Assert.assertEquals(MessageDecoder.MESSAGE_MAGIC_CODE_V2, MessageFormatUtil.getMagicCode(buffer)); + Assert.assertEquals(6L, MessageFormatUtil.getQueueOffset(buffer)); + Assert.assertEquals(7L, MessageFormatUtil.getCommitLogOffset(buffer)); + Assert.assertEquals(11L, MessageFormatUtil.getStoreTimeStamp(buffer)); + } + + @Test + public void getOffsetIdTest() { + ByteBuffer buffer = buildMockedMessageBuffer(); + InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 65535); + ByteBuffer address = ByteBuffer.allocate(Long.BYTES); + address.put(inetSocketAddress.getAddress().getAddress(), 0, 4); + address.putInt(inetSocketAddress.getPort()); + address.flip(); + for (int i = 0; i < address.remaining(); i++) { + buffer.put(MessageFormatUtil.STORE_HOST_POSITION + i, address.get(i)); + } + String excepted = MessageDecoder.createMessageId( + ByteBuffer.allocate(MessageFormatUtil.MSG_ID_LENGTH), address, 7); + String offsetId = MessageFormatUtil.getOffsetId(buffer); + Assert.assertEquals(excepted, offsetId); + } + + @Test + public void getPropertiesTest() { + ByteBuffer buffer = buildMockedMessageBuffer(); + Map properties = MessageFormatUtil.getProperties(buffer); + Assert.assertEquals(2, properties.size()); + Assert.assertTrue(properties.containsKey(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX)); + Assert.assertEquals("uk", properties.get(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX)); + Assert.assertTrue(properties.containsKey("UserKey")); + Assert.assertEquals("UserValue0", properties.get("UserKey")); + } + + @Test + public void testSplitMessages() { + ByteBuffer msgBuffer1 = buildMockedMessageBuffer(); + msgBuffer1.putLong(MessageFormatUtil.QUEUE_OFFSET_POSITION, 10); + + ByteBuffer msgBuffer2 = ByteBuffer.allocate(COMMIT_LOG_CODA_SIZE); + msgBuffer2.putInt(MessageFormatUtil.COMMIT_LOG_CODA_SIZE); + msgBuffer2.putInt(MessageFormatUtil.BLANK_MAGIC_CODE); + msgBuffer2.putLong(System.currentTimeMillis()); + msgBuffer2.flip(); + + ByteBuffer msgBuffer3 = buildMockedMessageBuffer(); + msgBuffer3.putLong(MessageFormatUtil.QUEUE_OFFSET_POSITION, 11); + + ByteBuffer msgBuffer = ByteBuffer.allocate( + msgBuffer1.remaining() + msgBuffer2.remaining() + msgBuffer3.remaining()); + msgBuffer.put(msgBuffer1); + msgBuffer.put(msgBuffer2); + msgBuffer.put(msgBuffer3); + msgBuffer.flip(); + + ByteBuffer cqBuffer1 = ByteBuffer.allocate(MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE); + cqBuffer1.putLong(1000); + cqBuffer1.putInt(MSG_LEN); + cqBuffer1.putLong(0); + cqBuffer1.flip(); + + ByteBuffer cqBuffer2 = ByteBuffer.allocate(MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE); + cqBuffer2.putLong(1000 + MessageFormatUtil.COMMIT_LOG_CODA_SIZE + MSG_LEN); + cqBuffer2.putInt(MSG_LEN); + cqBuffer2.putLong(0); + cqBuffer2.flip(); + + ByteBuffer cqBuffer3 = ByteBuffer.allocate(MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE); + cqBuffer3.putLong(1000 + MSG_LEN); + cqBuffer3.putInt(MSG_LEN); + cqBuffer3.putLong(0); + cqBuffer3.flip(); + + ByteBuffer cqBuffer4 = ByteBuffer.allocate(MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE); + cqBuffer4.putLong(1000 + MessageFormatUtil.COMMIT_LOG_CODA_SIZE + MSG_LEN); + cqBuffer4.putInt(MSG_LEN - 10); + cqBuffer4.putLong(0); + cqBuffer4.flip(); + + ByteBuffer cqBuffer5 = ByteBuffer.allocate(MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE); + cqBuffer5.putLong(1000 + MessageFormatUtil.COMMIT_LOG_CODA_SIZE + MSG_LEN); + cqBuffer5.putInt(MSG_LEN * 10); + cqBuffer5.putLong(0); + cqBuffer5.flip(); + + // Message buffer size is 0 or consume queue buffer size is 0 + Assert.assertEquals(0, + MessageFormatUtil.splitMessageBuffer(null, ByteBuffer.allocate(0)).size()); + Assert.assertEquals(0, + MessageFormatUtil.splitMessageBuffer(cqBuffer1, null).size()); + Assert.assertEquals(0, + MessageFormatUtil.splitMessageBuffer(cqBuffer1, ByteBuffer.allocate(0)).size()); + Assert.assertEquals(0, + MessageFormatUtil.splitMessageBuffer(ByteBuffer.allocate(0), msgBuffer).size()); + Assert.assertEquals(0, + MessageFormatUtil.splitMessageBuffer(ByteBuffer.allocate(10), msgBuffer).size()); + + ByteBuffer cqBuffer = ByteBuffer.allocate(MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE * 2); + cqBuffer.put(cqBuffer1); + cqBuffer.put(cqBuffer2); + cqBuffer.flip(); + cqBuffer1.rewind(); + cqBuffer2.rewind(); + List msgList = MessageFormatUtil.splitMessageBuffer(cqBuffer, msgBuffer); + Assert.assertEquals(2, msgList.size()); + Assert.assertEquals(0, msgList.get(0).getStartOffset()); + Assert.assertEquals(MSG_LEN, msgList.get(0).getSize()); + Assert.assertEquals(MSG_LEN + MessageFormatUtil.COMMIT_LOG_CODA_SIZE, msgList.get(1).getStartOffset()); + Assert.assertEquals(MSG_LEN, msgList.get(1).getSize()); + + cqBuffer = ByteBuffer.allocate(MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE * 2); + cqBuffer.put(cqBuffer1); + cqBuffer.put(cqBuffer4); + cqBuffer.flip(); + cqBuffer1.rewind(); + cqBuffer4.rewind(); + msgList = MessageFormatUtil.splitMessageBuffer(cqBuffer, msgBuffer); + Assert.assertEquals(1, msgList.size()); + Assert.assertEquals(0, msgList.get(0).getStartOffset()); + Assert.assertEquals(MSG_LEN, msgList.get(0).getSize()); + + cqBuffer = ByteBuffer.allocate(MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE * 3); + cqBuffer.put(cqBuffer1); + cqBuffer.put(cqBuffer3); + cqBuffer.flip(); + cqBuffer1.rewind(); + cqBuffer3.rewind(); + msgList = MessageFormatUtil.splitMessageBuffer(cqBuffer, msgBuffer); + Assert.assertEquals(2, msgList.size()); + Assert.assertEquals(0, msgList.get(0).getStartOffset()); + Assert.assertEquals(MSG_LEN, msgList.get(0).getSize()); + Assert.assertEquals(MSG_LEN + MessageFormatUtil.COMMIT_LOG_CODA_SIZE, msgList.get(1).getStartOffset()); + Assert.assertEquals(MSG_LEN, msgList.get(1).getSize()); + + cqBuffer = ByteBuffer.allocate(MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE); + cqBuffer.put(cqBuffer5); + cqBuffer.flip(); + msgList = MessageFormatUtil.splitMessageBuffer(cqBuffer, msgBuffer); + Assert.assertEquals(0, msgList.size()); + + // Wrong magic code, it will destroy the mocked message buffer + msgBuffer.putInt(MessageFormatUtil.MAGIC_CODE_POSITION, -1); + cqBuffer = ByteBuffer.allocate(MessageFormatUtil.CONSUME_QUEUE_UNIT_SIZE * 2); + cqBuffer.put(cqBuffer1); + cqBuffer.put(cqBuffer2); + cqBuffer.flip(); + cqBuffer1.rewind(); + cqBuffer2.rewind(); + Assert.assertEquals(1, MessageFormatUtil.splitMessageBuffer(cqBuffer, msgBuffer).size()); + } +} diff --git a/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/util/MessageStoreUtilTest.java b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/util/MessageStoreUtilTest.java new file mode 100644 index 0000000..cadaef8 --- /dev/null +++ b/tieredstore/src/test/java/org/apache/rocketmq/tieredstore/util/MessageStoreUtilTest.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.tieredstore.util; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import org.apache.commons.io.FileUtils; +import org.apache.rocketmq.common.message.MessageQueue; +import org.junit.Assert; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MessageStoreUtilTest { + + private static final Logger log = LoggerFactory.getLogger(MessageStoreUtil.TIERED_STORE_LOGGER_NAME); + private static final String TIERED_STORE_PATH = "tiered_store_test"; + + public static String getRandomStorePath() { + return Paths.get(System.getProperty("user.home"), TIERED_STORE_PATH, + UUID.randomUUID().toString().replace("-", "").toUpperCase().substring(0, 16)).toString(); + } + + public static void deleteStoreDirectory(String storePath) { + try { + FileUtils.deleteDirectory(new File(storePath)); + } catch (IOException e) { + log.error("Delete store directory failed, filePath: {}", storePath, e); + } + } + + @Test + public void toHumanReadableTest() { + Map capacityTable = new HashMap() { + { + put(-1L, "-1"); + put(0L, "0B"); + put(1023L, "1023B"); + put(1024L, "1KB"); + put(12_345L, "12.06KB"); + put(10_123_456L, "9.65MB"); + put(10_123_456_798L, "9.43GB"); + put(123 * 1024L * 1024L * 1024L * 1024L, "123TB"); + put(123 * 1024L * 1024L * 1024L * 1024L * 1024L, "123PB"); + put(1_777_777_777_777_777_777L, "1.54EB"); + } + }; + capacityTable.forEach((in, expected) -> + Assert.assertEquals(expected, MessageStoreUtil.toHumanReadable(in))); + } + + @Test + public void getHashTest() { + Assert.assertEquals("161c08ff", MessageStoreUtil.getHash("TieredStorageDailyTest")); + } + + @Test + public void filePathTest() { + MessageQueue mq = new MessageQueue(); + mq.setBrokerName("BrokerName"); + mq.setTopic("topicName"); + mq.setQueueId(2); + Assert.assertEquals("BrokerName/topicName/2", MessageStoreUtil.toFilePath(mq)); + } + + @Test + public void offset2FileNameTest() { + Assert.assertEquals("cfcd208400000000000000000000", MessageStoreUtil.offset2FileName(0)); + Assert.assertEquals("b10da56800000000004294937144", MessageStoreUtil.offset2FileName(4294937144L)); + } + + @Test + public void fileName2OffsetTest() { + Assert.assertEquals(0, MessageStoreUtil.fileName2Offset("cfcd208400000000000000000000")); + Assert.assertEquals(4294937144L, MessageStoreUtil.fileName2Offset("b10da56800000000004294937144")); + } + + @Test + public void indexServicePathTest() { + Assert.assertEquals("brokerName/rmq_sys_INDEX/0", MessageStoreUtil.getIndexFilePath("brokerName")); + } +} diff --git a/tieredstore/src/test/resources/rmq.logback-test.xml b/tieredstore/src/test/resources/rmq.logback-test.xml new file mode 100644 index 0000000..070bf13 --- /dev/null +++ b/tieredstore/src/test/resources/rmq.logback-test.xml @@ -0,0 +1,40 @@ + + + + + + ${CONSOLE_LOG_PATTERN} + + + + + + + + + + + + + + + + + diff --git a/tieredstore/tiered_storage_arch.png b/tieredstore/tiered_storage_arch.png new file mode 100644 index 0000000..05efac7 Binary files /dev/null and b/tieredstore/tiered_storage_arch.png differ diff --git a/tools/BUILD.bazel b/tools/BUILD.bazel new file mode 100644 index 0000000..ec6fa1e --- /dev/null +++ b/tools/BUILD.bazel @@ -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. +# +load("//bazel:GenTestRules.bzl", "GenTestRules") + +java_library( + name = "tools", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//remoting", + "//client", + "//common", + "//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//:commons_cli_commons_cli", + "@maven//:org_slf4j_slf4j_api", + "@maven//:ch_qos_logback_logback_classic", + "@maven//:ch_qos_logback_logback_core", + "@maven//:commons_collections_commons_collections", + "@maven//:io_github_aliyunmq_rocketmq_slf4j_api", + "@maven//:io_github_aliyunmq_rocketmq_logback_classic", + "@maven//:org_apache_rocketmq_rocketmq_rocksdb", + "@maven//:com_alibaba_fastjson2_fastjson2", + ], +) + +java_library( + name = "tests", + srcs = glob(["src/test/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + ":tools", + "//client", + "//common", + "//srvutil", + "//remoting", + "//:test_deps", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:io_netty_netty_all", + "@maven//:commons_cli_commons_cli", + "@maven//:org_junit_jupiter_junit_jupiter_api", + ], + resources = glob(["src/test/resources/*.xml"]), +) + +GenTestRules( + name = "GeneratedTestRules", + test_files = glob(["src/test/java/**/*Test.java"]), + deps = [ + ":tests", + ], +) diff --git a/tools/pom.xml b/tools/pom.xml new file mode 100644 index 0000000..1f0bef3 --- /dev/null +++ b/tools/pom.xml @@ -0,0 +1,64 @@ + + + + + org.apache.rocketmq + rocketmq-all + 5.3.4-SNAPSHOT + + + 4.0.0 + jar + rocketmq-tools + rocketmq-tools ${project.version} + + + ${basedir}/.. + + + + + ${project.groupId} + rocketmq-client + + + ${project.groupId} + rocketmq-auth + + + ${project.groupId} + rocketmq-srvutil + + + com.alibaba + fastjson + + + org.apache.commons + commons-lang3 + + + org.yaml + snakeyaml + + + com.google.guava + guava + + + diff --git a/tools/src/main/java/org/apache/rocketmq/tools/admin/DefaultMQAdminExt.java b/tools/src/main/java/org/apache/rocketmq/tools/admin/DefaultMQAdminExt.java new file mode 100644 index 0000000..9780df1 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/admin/DefaultMQAdminExt.java @@ -0,0 +1,989 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tools.admin; + +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.QueryResult; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.common.CheckRocksdbCqWriteResult; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.message.MessageRequestMode; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.remoting.RPCHook; +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.protocol.admin.ConsumeStats; +import org.apache.rocketmq.remoting.protocol.admin.RollbackStats; +import org.apache.rocketmq.remoting.protocol.admin.TopicStatsTable; +import org.apache.rocketmq.remoting.protocol.body.AclInfo; +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.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.GroupList; +import org.apache.rocketmq.remoting.protocol.body.HARuntimeInfo; +import org.apache.rocketmq.remoting.protocol.body.KVTable; +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.QueueTimeSpan; +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.UserInfo; +import org.apache.rocketmq.remoting.protocol.header.ExportRocksDBConfigToJsonRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetMetaDataResponseHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +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.tools.admin.api.BrokerOperatorResult; +import org.apache.rocketmq.tools.admin.api.MessageTrack; +import org.apache.rocketmq.tools.admin.common.AdminToolResult; + +import java.io.UnsupportedEncodingException; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +public class DefaultMQAdminExt extends ClientConfig implements MQAdminExt { + private final DefaultMQAdminExtImpl defaultMQAdminExtImpl; + private String adminExtGroup = "admin_ext_group"; + private String createTopicKey = TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC; + private long timeoutMillis = 5000; + + public DefaultMQAdminExt() { + this.defaultMQAdminExtImpl = new DefaultMQAdminExtImpl(this, null, timeoutMillis); + } + + public DefaultMQAdminExt(long timeoutMillis) { + this.defaultMQAdminExtImpl = new DefaultMQAdminExtImpl(this, null, timeoutMillis); + } + + public DefaultMQAdminExt(RPCHook rpcHook) { + this.defaultMQAdminExtImpl = new DefaultMQAdminExtImpl(this, rpcHook, timeoutMillis); + } + + public DefaultMQAdminExt(RPCHook rpcHook, long timeoutMillis) { + this.defaultMQAdminExtImpl = new DefaultMQAdminExtImpl(this, rpcHook, timeoutMillis); + } + + public DefaultMQAdminExt(final String adminExtGroup) { + this.adminExtGroup = adminExtGroup; + this.defaultMQAdminExtImpl = new DefaultMQAdminExtImpl(this, timeoutMillis); + } + + public DefaultMQAdminExt(final String adminExtGroup, long timeoutMillis) { + this.adminExtGroup = adminExtGroup; + this.defaultMQAdminExtImpl = new DefaultMQAdminExtImpl(this, timeoutMillis); + } + + @Override + public void createTopic(String key, String newTopic, int queueNum, + Map attributes) throws MQClientException { + createTopic(key, newTopic, queueNum, 0, attributes); + } + + @Override + public void createTopic(String key, String newTopic, int queueNum, int topicSysFlag, + Map attributes) throws MQClientException { + defaultMQAdminExtImpl.createTopic(key, newTopic, queueNum, topicSysFlag, attributes); + } + + @Override + public long searchOffset(MessageQueue mq, long timestamp) throws MQClientException { + return defaultMQAdminExtImpl.searchOffset(mq, timestamp); + } + + public long searchLowerBoundaryOffset(MessageQueue mq, long timestamp) throws MQClientException { + return defaultMQAdminExtImpl.searchOffset(mq, timestamp, BoundaryType.LOWER); + } + + public long searchUpperBoundaryOffset(MessageQueue mq, long timestamp) throws MQClientException { + return defaultMQAdminExtImpl.searchOffset(mq, timestamp, BoundaryType.UPPER); + } + + @Override + public long maxOffset(MessageQueue mq) throws MQClientException { + return defaultMQAdminExtImpl.maxOffset(mq); + } + + @Override + public long minOffset(MessageQueue mq) throws MQClientException { + return defaultMQAdminExtImpl.minOffset(mq); + } + + @Override + public long earliestMsgStoreTime(MessageQueue mq) throws MQClientException { + return defaultMQAdminExtImpl.earliestMsgStoreTime(mq); + } + + @Override + public QueryResult queryMessage(String topic, String key, int maxNum, long begin, long end) + throws MQClientException, InterruptedException { + return defaultMQAdminExtImpl.queryMessage(topic, key, maxNum, begin, end); + } + + + public QueryResult queryMessage(String clusterName, String topic, String key, int maxNum, long begin, long end) + throws MQClientException, InterruptedException, RemotingException { + return defaultMQAdminExtImpl.queryMessage(clusterName, topic, key, maxNum, begin, end); + } + + @Override + public void start() throws MQClientException { + defaultMQAdminExtImpl.start(); + } + + @Override + public void shutdown() { + defaultMQAdminExtImpl.shutdown(); + } + + @Override + public void addBrokerToContainer(String brokerContainerAddr, String brokerConfig) throws InterruptedException, + RemotingConnectException, RemotingTimeoutException, RemotingSendRequestException, MQBrokerException { + defaultMQAdminExtImpl.addBrokerToContainer(brokerContainerAddr, brokerConfig); + } + + @Override + public void removeBrokerFromContainer(String brokerContainerAddr, String clusterName, String brokerName, + long brokerId) throws InterruptedException, MQBrokerException, RemotingTimeoutException, RemotingSendRequestException, + RemotingConnectException { + defaultMQAdminExtImpl.removeBrokerFromContainer(brokerContainerAddr, clusterName, brokerName, brokerId); + } + + @Override + public void updateBrokerConfig(String brokerAddr, + Properties properties) throws RemotingConnectException, RemotingSendRequestException, + RemotingTimeoutException, UnsupportedEncodingException, InterruptedException, MQBrokerException, MQClientException { + defaultMQAdminExtImpl.updateBrokerConfig(brokerAddr, properties); + } + + @Override + public Properties getBrokerConfig(final String brokerAddr) throws RemotingConnectException, + RemotingSendRequestException, RemotingTimeoutException, UnsupportedEncodingException, InterruptedException, MQBrokerException { + return defaultMQAdminExtImpl.getBrokerConfig(brokerAddr); + } + + @Override + public void createAndUpdateTopicConfig(String addr, TopicConfig config) throws RemotingException, MQBrokerException, + InterruptedException, MQClientException { + defaultMQAdminExtImpl.createAndUpdateTopicConfig(addr, config); + } + + @Override + public void createAndUpdateTopicConfigList(String addr, + List topicConfigList) throws InterruptedException, RemotingException, MQClientException { + defaultMQAdminExtImpl.createAndUpdateTopicConfigList(addr, topicConfigList); + } + + @Override + public void createAndUpdateSubscriptionGroupConfig(String addr, + SubscriptionGroupConfig config) throws RemotingException, + MQBrokerException, InterruptedException, MQClientException { + defaultMQAdminExtImpl.createAndUpdateSubscriptionGroupConfig(addr, config); + } + + @Override + public void createAndUpdateSubscriptionGroupConfigList(String brokerAddr, + List configs) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + defaultMQAdminExtImpl.createAndUpdateSubscriptionGroupConfigList(brokerAddr, configs); + } + + @Override + public SubscriptionGroupConfig examineSubscriptionGroupConfig(String addr, String group) + throws InterruptedException, RemotingException, MQClientException, MQBrokerException { + return defaultMQAdminExtImpl.examineSubscriptionGroupConfig(addr, group); + } + + @Override + public TopicConfig examineTopicConfig(String addr, + String topic) throws RemotingSendRequestException, RemotingConnectException, RemotingTimeoutException, InterruptedException, MQBrokerException { + return defaultMQAdminExtImpl.examineTopicConfig(addr, topic); + } + + @Override + public TopicStatsTable examineTopicStats( + String topic) throws RemotingException, MQClientException, InterruptedException, + MQBrokerException { + return defaultMQAdminExtImpl.examineTopicStats(topic); + } + + @Override + public TopicStatsTable examineTopicStats(String brokerAddr, + String topic) throws RemotingException, MQClientException, InterruptedException, + MQBrokerException { + return defaultMQAdminExtImpl.examineTopicStats(brokerAddr, topic); + } + + @Override + public AdminToolResult examineTopicStatsConcurrent(String topic) { + return defaultMQAdminExtImpl.examineTopicStatsConcurrent(topic); + } + + @Override + public TopicList fetchAllTopicList() throws RemotingException, MQClientException, InterruptedException { + return this.defaultMQAdminExtImpl.fetchAllTopicList(); + } + + @Override + public TopicList fetchTopicsByCLuster( + String clusterName) throws RemotingException, MQClientException, InterruptedException { + return this.defaultMQAdminExtImpl.fetchTopicsByCLuster(clusterName); + } + + @Override + public KVTable fetchBrokerRuntimeStats( + final String brokerAddr) throws RemotingConnectException, RemotingSendRequestException, + RemotingTimeoutException, InterruptedException, MQBrokerException { + return this.defaultMQAdminExtImpl.fetchBrokerRuntimeStats(brokerAddr); + } + + @Override + public ConsumeStats examineConsumeStats( + String consumerGroup) throws RemotingException, MQClientException, InterruptedException, + MQBrokerException { + return examineConsumeStats(consumerGroup, null); + } + + @Override + public ConsumeStats examineConsumeStats(String clusterName, String consumerGroup, + String topic) throws RemotingException, MQClientException, InterruptedException, MQBrokerException { + return defaultMQAdminExtImpl.examineConsumeStats(clusterName, consumerGroup, topic); + } + + @Override + public ConsumeStats examineConsumeStats(String consumerGroup, + String topic) throws RemotingException, MQClientException, + InterruptedException, MQBrokerException { + return defaultMQAdminExtImpl.examineConsumeStats(consumerGroup, topic); + } + + @Override + public ConsumeStats examineConsumeStats(final String brokerAddr, final String consumerGroup, + final String topicName, final long timeoutMillis) + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQBrokerException { + return this.defaultMQAdminExtImpl.examineConsumeStats(brokerAddr, consumerGroup, topicName, timeoutMillis); + } + + @Override + public AdminToolResult examineConsumeStatsConcurrent(String consumerGroup, String topic) { + return defaultMQAdminExtImpl.examineConsumeStatsConcurrent(consumerGroup, topic); + } + + @Override + public ClusterInfo examineBrokerClusterInfo() throws InterruptedException, RemotingConnectException, RemotingTimeoutException, + RemotingSendRequestException, MQBrokerException { + return defaultMQAdminExtImpl.examineBrokerClusterInfo(); + } + + @Override + public TopicRouteData examineTopicRouteInfo( + String topic) throws RemotingException, MQClientException, InterruptedException { + return defaultMQAdminExtImpl.examineTopicRouteInfo(topic); + } + + @Override + public ConsumerConnection examineConsumerConnectionInfo( + String consumerGroup) throws InterruptedException, MQBrokerException, + RemotingException, MQClientException { + return defaultMQAdminExtImpl.examineConsumerConnectionInfo(consumerGroup); + } + + @Override + public ConsumerConnection examineConsumerConnectionInfo( + String consumerGroup, String brokerAddr) throws InterruptedException, MQBrokerException, + RemotingException, MQClientException { + return defaultMQAdminExtImpl.examineConsumerConnectionInfo(consumerGroup, brokerAddr); + } + + @Override + public ProducerConnection examineProducerConnectionInfo(String producerGroup, + final String topic) throws RemotingException, + MQClientException, InterruptedException, MQBrokerException { + return defaultMQAdminExtImpl.examineProducerConnectionInfo(producerGroup, topic); + } + + @Override + public ProducerTableInfo getAllProducerInfo( + final String brokerAddr) throws RemotingException, MQClientException, InterruptedException, MQBrokerException { + return defaultMQAdminExtImpl.getAllProducerInfo(brokerAddr); + } + + @Override + public void deleteTopicInNameServer(Set addrs, String clusterName, + String topic) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + defaultMQAdminExtImpl.deleteTopicInNameServer(addrs, clusterName, topic); + } + + @Override + public List getNameServerAddressList() { + return this.defaultMQAdminExtImpl.getNameServerAddressList(); + } + + @Override + public int wipeWritePermOfBroker(final String namesrvAddr, String brokerName) throws RemotingCommandException, + RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQClientException { + return defaultMQAdminExtImpl.wipeWritePermOfBroker(namesrvAddr, brokerName); + } + + @Override + public int addWritePermOfBroker(String namesrvAddr, + String brokerName) throws RemotingCommandException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQClientException { + return defaultMQAdminExtImpl.addWritePermOfBroker(namesrvAddr, brokerName); + } + + @Override + public void putKVConfig(String namespace, String key, String value) { + defaultMQAdminExtImpl.putKVConfig(namespace, key, value); + } + + @Override + public String getKVConfig(String namespace, + String key) throws RemotingException, MQClientException, InterruptedException { + return defaultMQAdminExtImpl.getKVConfig(namespace, key); + } + + @Override + public KVTable getKVListByNamespace( + String namespace) throws RemotingException, MQClientException, InterruptedException { + return defaultMQAdminExtImpl.getKVListByNamespace(namespace); + } + + @Override + public void deleteTopic(String topicName, + String clusterName) throws RemotingException, MQBrokerException, InterruptedException, + MQClientException { + defaultMQAdminExtImpl.deleteTopic(topicName, clusterName); + } + + @Override + public void deleteTopicInBroker(Set addrs, + String topic) throws RemotingException, MQBrokerException, InterruptedException, + MQClientException { + defaultMQAdminExtImpl.deleteTopicInBroker(addrs, topic); + } + + @Override + public AdminToolResult deleteTopicInBrokerConcurrent(Set addrs, String topic) { + return defaultMQAdminExtImpl.deleteTopicInBrokerConcurrent(addrs, topic); + } + + @Override + public void deleteTopicInNameServer(Set addrs, + String topic) throws RemotingException, MQBrokerException, InterruptedException, + MQClientException { + defaultMQAdminExtImpl.deleteTopicInNameServer(addrs, topic); + } + + @Override + public void deleteSubscriptionGroup(String addr, + String groupName) throws RemotingException, MQBrokerException, InterruptedException, + MQClientException { + defaultMQAdminExtImpl.deleteSubscriptionGroup(addr, groupName); + } + + @Override + public void deleteSubscriptionGroup(String addr, + String groupName, boolean removeOffset) throws RemotingException, MQBrokerException, InterruptedException, + MQClientException { + defaultMQAdminExtImpl.deleteSubscriptionGroup(addr, groupName, removeOffset); + } + + @Override + public void createAndUpdateKvConfig(String namespace, String key, + String value) throws RemotingException, MQBrokerException, + InterruptedException, MQClientException { + defaultMQAdminExtImpl.createAndUpdateKvConfig(namespace, key, value); + } + + @Override + public void deleteKvConfig(String namespace, + String key) throws RemotingException, MQBrokerException, InterruptedException, + MQClientException { + defaultMQAdminExtImpl.deleteKvConfig(namespace, key); + } + + @Override + public List resetOffsetByTimestampOld(String consumerGroup, String topic, long timestamp, + boolean force) + throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + return defaultMQAdminExtImpl.resetOffsetByTimestampOld(consumerGroup, topic, timestamp, force); + } + + public List resetOffsetByTimestampOld(String clusterName, String consumerGroup, String topic, long timestamp, + boolean force) + throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + return defaultMQAdminExtImpl.resetOffsetByTimestampOld(clusterName, consumerGroup, topic, timestamp, force); + } + + @Override + public Map resetOffsetByTimestamp(String clusterName, String topic, String group, + long timestamp, boolean isForce) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + return defaultMQAdminExtImpl.resetOffsetByTimestamp(clusterName, topic, group, timestamp, isForce); + } + + @Override + public Map resetOffsetByTimestamp(String topic, String group, long timestamp, boolean isForce) + throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + return resetOffsetByTimestamp(topic, group, timestamp, isForce, false); + } + + public Map resetOffsetByTimestamp(String clusterName, String topic, String group, + long timestamp, boolean isForce, boolean isC) + throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + return defaultMQAdminExtImpl.resetOffsetByTimestamp(clusterName, topic, group, timestamp, isForce, isC); + } + + + public Map resetOffsetByTimestamp(String topic, String group, long timestamp, boolean isForce, + boolean isC) + throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + return defaultMQAdminExtImpl.resetOffsetByTimestamp(null, topic, group, timestamp, isForce, isC); + } + + @Override + public void resetOffsetNew(String consumerGroup, String topic, + long timestamp) throws RemotingException, MQBrokerException, + InterruptedException, MQClientException { + this.defaultMQAdminExtImpl.resetOffsetNew(consumerGroup, topic, timestamp); + } + + @Override + public AdminToolResult resetOffsetNewConcurrent(final String group, final String topic, + final long timestamp) { + return this.defaultMQAdminExtImpl.resetOffsetNewConcurrent(group, topic, timestamp); + } + + @Override + public Map> getConsumeStatus(String topic, String group, + String clientAddr) throws RemotingException, + MQBrokerException, InterruptedException, MQClientException { + return defaultMQAdminExtImpl.getConsumeStatus(topic, group, clientAddr); + } + + @Override + public void createOrUpdateOrderConf(String key, String value, + boolean isCluster) throws RemotingException, MQBrokerException, + InterruptedException, MQClientException { + defaultMQAdminExtImpl.createOrUpdateOrderConf(key, value, isCluster); + } + + @Override + public GroupList queryTopicConsumeByWho( + String topic) throws InterruptedException, MQBrokerException, RemotingException, + MQClientException { + return this.defaultMQAdminExtImpl.queryTopicConsumeByWho(topic); + } + + @Override + public TopicList queryTopicsByConsumer(String group) + throws InterruptedException, MQBrokerException, RemotingException, MQClientException { + return this.defaultMQAdminExtImpl.queryTopicsByConsumer(group); + } + + @Override + public AdminToolResult queryTopicsByConsumerConcurrent(String group) { + return defaultMQAdminExtImpl.queryTopicsByConsumerConcurrent(group); + } + + @Override + public SubscriptionData querySubscription(String group, String topic) + throws InterruptedException, MQBrokerException, RemotingException, MQClientException { + return this.defaultMQAdminExtImpl.querySubscription(group, topic); + } + + @Override + public List queryConsumeTimeSpan(final String topic, + final String group) throws InterruptedException, MQBrokerException, + RemotingException, MQClientException { + return this.defaultMQAdminExtImpl.queryConsumeTimeSpan(topic, group); + } + + @Override + public AdminToolResult> queryConsumeTimeSpanConcurrent(String topic, String group) { + return defaultMQAdminExtImpl.queryConsumeTimeSpanConcurrent(topic, group); + } + + @Override + public boolean cleanExpiredConsumerQueue( + String cluster) throws RemotingConnectException, RemotingSendRequestException, + RemotingTimeoutException, MQClientException, InterruptedException { + return defaultMQAdminExtImpl.cleanExpiredConsumerQueue(cluster); + } + + @Override + public boolean cleanExpiredConsumerQueueByAddr( + String addr) throws RemotingConnectException, RemotingSendRequestException, + RemotingTimeoutException, MQClientException, InterruptedException { + return defaultMQAdminExtImpl.cleanExpiredConsumerQueueByAddr(addr); + } + + @Override + public boolean deleteExpiredCommitLog(String cluster) throws RemotingConnectException, RemotingSendRequestException, + RemotingTimeoutException, MQClientException, InterruptedException { + return defaultMQAdminExtImpl.deleteExpiredCommitLog(cluster); + } + + @Override + public boolean deleteExpiredCommitLogByAddr( + String addr) throws RemotingConnectException, RemotingSendRequestException, + RemotingTimeoutException, MQClientException, InterruptedException { + return defaultMQAdminExtImpl.deleteExpiredCommitLogByAddr(addr); + } + + @Override + public boolean cleanUnusedTopic(String cluster) throws RemotingConnectException, RemotingSendRequestException, + RemotingTimeoutException, MQClientException, InterruptedException { + return defaultMQAdminExtImpl.cleanUnusedTopic(cluster); + } + + @Override + public boolean cleanUnusedTopicByAddr(String addr) throws RemotingConnectException, RemotingSendRequestException, + RemotingTimeoutException, MQClientException, InterruptedException { + return defaultMQAdminExtImpl.cleanUnusedTopicByAddr(addr); + } + + @Override + public ConsumerRunningInfo getConsumerRunningInfo(String consumerGroup, String clientId, + boolean jstack) throws RemotingException, + MQClientException, InterruptedException { + return defaultMQAdminExtImpl.getConsumerRunningInfo(consumerGroup, clientId, jstack); + } + + @Override + public ConsumerRunningInfo getConsumerRunningInfo(String consumerGroup, String clientId, + boolean jstack, boolean metrics) throws RemotingException, + MQClientException, InterruptedException { + return defaultMQAdminExtImpl.getConsumerRunningInfo(consumerGroup, clientId, jstack, metrics); + } + + @Override + public ConsumeMessageDirectlyResult consumeMessageDirectly(final String consumerGroup, final String clientId, + final String topic, + final String msgId) throws RemotingException, MQClientException, InterruptedException, MQBrokerException { + return defaultMQAdminExtImpl.consumeMessageDirectly(consumerGroup, clientId, topic, msgId); + } + + @Override + public ConsumeMessageDirectlyResult consumeMessageDirectly(final String clusterName, final String consumerGroup, + final String clientId, + final String topic, + final String msgId) throws RemotingException, MQClientException, InterruptedException, MQBrokerException { + return defaultMQAdminExtImpl.consumeMessageDirectly(clusterName, consumerGroup, clientId, topic, msgId); + } + + @Override + public List messageTrackDetail( + MessageExt msg) throws RemotingException, MQClientException, InterruptedException, + MQBrokerException { + return this.defaultMQAdminExtImpl.messageTrackDetail(msg); + } + + @Override + public List messageTrackDetailConcurrent( + MessageExt msg) throws RemotingException, MQClientException, InterruptedException, + MQBrokerException { + return this.defaultMQAdminExtImpl.messageTrackDetailConcurrent(msg); + } + + @Override + public void cloneGroupOffset(String srcGroup, String destGroup, String topic, + boolean isOffline) throws RemotingException, + MQClientException, InterruptedException, MQBrokerException { + this.defaultMQAdminExtImpl.cloneGroupOffset(srcGroup, destGroup, topic, isOffline); + } + + @Override + public BrokerStatsData viewBrokerStatsData(String brokerAddr, String statsName, + String statsKey) throws RemotingConnectException, + RemotingSendRequestException, RemotingTimeoutException, MQClientException, InterruptedException { + return this.defaultMQAdminExtImpl.viewBrokerStatsData(brokerAddr, statsName, statsKey); + } + + @Override + public Set getClusterList(String topic) throws RemotingConnectException, RemotingSendRequestException, + RemotingTimeoutException, MQClientException, InterruptedException { + return this.defaultMQAdminExtImpl.getClusterList(topic); + } + + @Override + public ConsumeStatsList fetchConsumeStatsInBroker(final String brokerAddr, boolean isOrder, + long timeoutMillis) throws RemotingConnectException, RemotingSendRequestException, + RemotingTimeoutException, MQClientException, InterruptedException { + return this.defaultMQAdminExtImpl.fetchConsumeStatsInBroker(brokerAddr, isOrder, timeoutMillis); + } + + @Override + public Set getTopicClusterList( + final String topic) throws InterruptedException, MQBrokerException, MQClientException, RemotingException { + return this.defaultMQAdminExtImpl.getTopicClusterList(topic); + } + + @Override + public SubscriptionGroupWrapper getAllSubscriptionGroup(final String brokerAddr, + long timeoutMillis) throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, + RemotingConnectException, MQBrokerException { + return this.defaultMQAdminExtImpl.getAllSubscriptionGroup(brokerAddr, timeoutMillis); + } + + @Override + public SubscriptionGroupWrapper getUserSubscriptionGroup(final String brokerAddr, + long timeoutMillis) throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, + RemotingConnectException, MQBrokerException { + return this.defaultMQAdminExtImpl.getUserSubscriptionGroup(brokerAddr, timeoutMillis); + } + + @Override + public TopicConfigSerializeWrapper getAllTopicConfig(final String brokerAddr, + long timeoutMillis) throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, + RemotingConnectException, MQBrokerException { + return this.defaultMQAdminExtImpl.getAllTopicConfig(brokerAddr, timeoutMillis); + } + + @Override + public TopicConfigSerializeWrapper getUserTopicConfig(final String brokerAddr, final boolean specialTopic, + long timeoutMillis) throws InterruptedException, RemotingException, + MQBrokerException, MQClientException { + return this.defaultMQAdminExtImpl.getUserTopicConfig(brokerAddr, specialTopic, timeoutMillis); + } + + /* (non-Javadoc) + * @see org.apache.rocketmq.client.MQAdmin#queryMessageByUniqKey(java.lang.String, java.lang.String) + */ + @Override + public MessageExt viewMessage(String topic, String msgId) + throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + return this.defaultMQAdminExtImpl.viewMessage(topic, msgId); + } + + @Override + public MessageExt queryMessage(String clusterName, String topic, String msgId) + throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + return this.defaultMQAdminExtImpl.queryMessage(clusterName, topic, msgId); + } + + public String getAdminExtGroup() { + return adminExtGroup; + } + + public void setAdminExtGroup(String adminExtGroup) { + this.adminExtGroup = adminExtGroup; + } + + public String getCreateTopicKey() { + return createTopicKey; + } + + public void setCreateTopicKey(String createTopicKey) { + this.createTopicKey = createTopicKey; + } + + @Override + public void updateConsumeOffset(String brokerAddr, String consumeGroup, MessageQueue mq, + long offset) throws RemotingException, InterruptedException, MQBrokerException { + this.defaultMQAdminExtImpl.updateConsumeOffset(brokerAddr, consumeGroup, mq, offset); + } + + @Override + public void updateNameServerConfig(final Properties properties, final List nameServers) + throws InterruptedException, RemotingConnectException, + UnsupportedEncodingException, MQBrokerException, RemotingTimeoutException, + MQClientException, RemotingSendRequestException { + this.defaultMQAdminExtImpl.updateNameServerConfig(properties, nameServers); + } + + @Override + public Map getNameServerConfig(final List nameServers) + throws InterruptedException, RemotingTimeoutException, + RemotingSendRequestException, RemotingConnectException, MQClientException, + UnsupportedEncodingException { + return this.defaultMQAdminExtImpl.getNameServerConfig(nameServers); + } + + @Override + public QueryConsumeQueueResponseBody queryConsumeQueue(String brokerAddr, String topic, int queueId, long index, + int count, String consumerGroup) + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQClientException { + return this.defaultMQAdminExtImpl.queryConsumeQueue( + brokerAddr, topic, queueId, index, count, consumerGroup + ); + } + + @Override + public CheckRocksdbCqWriteResult checkRocksdbCqWriteProgress(String brokerAddr, String topic, long checkStoreTime) + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQClientException { + return this.defaultMQAdminExtImpl.checkRocksdbCqWriteProgress(brokerAddr, topic, checkStoreTime); + } + + @Override + public void exportRocksDBConfigToJson(String brokerAddr, + List configType) + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQClientException { + this.defaultMQAdminExtImpl.exportRocksDBConfigToJson(brokerAddr, configType); + } + + @Override + public boolean resumeCheckHalfMessage(String topic, + String msgId) + throws RemotingException, MQClientException, InterruptedException, MQBrokerException { + return this.defaultMQAdminExtImpl.resumeCheckHalfMessage(topic, msgId); + } + + @Override + 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 { + this.defaultMQAdminExtImpl.setMessageRequestMode(brokerAddr, topic, consumerGroup, mode, popShareQueueNum, timeoutMillis); + } + + @Override + public void createStaticTopic(String addr, String defaultTopic, TopicConfig topicConfig, + TopicQueueMappingDetail mappingDetail, + boolean force) throws RemotingException, InterruptedException, MQBrokerException { + this.defaultMQAdminExtImpl.createStaticTopic(addr, defaultTopic, topicConfig, mappingDetail, force); + } + + @Deprecated + @Override + public long searchOffset(final String brokerAddr, final String topicName, + final int queueId, final long timestamp, final long timeoutMillis) + throws RemotingException, MQBrokerException, InterruptedException { + return this.defaultMQAdminExtImpl.searchOffset(brokerAddr, topicName, queueId, timestamp, timeoutMillis); + } + + @Override + public void resetOffsetByQueueId(final String brokerAddr, final String consumerGroup, + final String topicName, final int queueId, final long resetOffset) + throws RemotingException, InterruptedException, MQBrokerException { + this.defaultMQAdminExtImpl.resetOffsetByQueueId(brokerAddr, consumerGroup, topicName, queueId, resetOffset); + } + + @Override + public HARuntimeInfo getBrokerHAStatus(String brokerAddr) throws RemotingConnectException, + RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException { + return this.defaultMQAdminExtImpl.getBrokerHAStatus(brokerAddr); + } + + @Override + public BrokerReplicasInfo getInSyncStateData(String controllerAddress, + List brokers) throws RemotingException, InterruptedException, MQBrokerException { + return this.defaultMQAdminExtImpl.getInSyncStateData(controllerAddress, brokers); + } + + @Override + public EpochEntryCache getBrokerEpochCache( + String brokerAddr) throws RemotingException, InterruptedException, MQBrokerException { + return this.defaultMQAdminExtImpl.getBrokerEpochCache(brokerAddr); + } + + public GetMetaDataResponseHeader getControllerMetaData( + String controllerAddr) throws RemotingException, InterruptedException, MQBrokerException { + return this.defaultMQAdminExtImpl.getControllerMetaData(controllerAddr); + } + + @Override + public void resetMasterFlushOffset(String brokerAddr, long masterFlushOffset) + throws InterruptedException, RemotingConnectException, RemotingTimeoutException, RemotingSendRequestException, MQBrokerException { + this.defaultMQAdminExtImpl.resetMasterFlushOffset(brokerAddr, masterFlushOffset); + } + + public QueryResult queryMessageByUniqKey(String clusterName, String topic, String key, int maxNum, long begin, + long end) + throws MQClientException, InterruptedException { + return defaultMQAdminExtImpl.queryMessageByUniqKey(clusterName, topic, key, maxNum, begin, end); + } + + public DefaultMQAdminExtImpl getDefaultMQAdminExtImpl() { + return defaultMQAdminExtImpl; + } + + public GroupForbidden updateAndGetGroupReadForbidden(String brokerAddr, // + String groupName, // + String topicName, // + Boolean readable) // + throws RemotingException, + InterruptedException, MQBrokerException { + return this.defaultMQAdminExtImpl.updateAndGetGroupReadForbidden(brokerAddr, groupName, topicName, readable); + } + + @Override + public Map getControllerConfig( + List controllerServers) throws InterruptedException, RemotingTimeoutException, + RemotingSendRequestException, RemotingConnectException, MQClientException, + UnsupportedEncodingException { + return this.defaultMQAdminExtImpl.getControllerConfig(controllerServers); + } + + @Override + public void updateControllerConfig(Properties properties, + List controllers) throws InterruptedException, RemotingConnectException, UnsupportedEncodingException, RemotingSendRequestException, RemotingTimeoutException, MQClientException, MQBrokerException { + this.defaultMQAdminExtImpl.updateControllerConfig(properties, controllers); + } + + @Override + public Pair electMaster(String controllerAddr, String clusterName, + String brokerName, Long brokerId) throws RemotingException, InterruptedException, MQBrokerException { + return this.defaultMQAdminExtImpl.electMaster(controllerAddr, clusterName, brokerName, brokerId); + } + + @Override + public void cleanControllerBrokerData(String controllerAddr, String clusterName, String brokerName, + String brokerControllerIdsToClean, + boolean isCleanLivingBroker) throws RemotingException, InterruptedException, MQBrokerException { + this.defaultMQAdminExtImpl.cleanControllerBrokerData(controllerAddr, clusterName, brokerName, brokerControllerIdsToClean, isCleanLivingBroker); + } + + @Override + public void updateColdDataFlowCtrGroupConfig(String brokerAddr, Properties properties) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, + UnsupportedEncodingException, InterruptedException, MQBrokerException { + defaultMQAdminExtImpl.updateColdDataFlowCtrGroupConfig(brokerAddr, properties); + } + + @Override + public void removeColdDataFlowCtrGroupConfig(String brokerAddr, String consumerGroup) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, + UnsupportedEncodingException, InterruptedException, MQBrokerException { + defaultMQAdminExtImpl.removeColdDataFlowCtrGroupConfig(brokerAddr, consumerGroup); + } + + @Override + public String getColdDataFlowCtrInfo(String brokerAddr) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, + UnsupportedEncodingException, InterruptedException, MQBrokerException { + return defaultMQAdminExtImpl.getColdDataFlowCtrInfo(brokerAddr); + } + + @Override + public String setCommitLogReadAheadMode(String brokerAddr, String mode) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, + InterruptedException, MQBrokerException { + return defaultMQAdminExtImpl.setCommitLogReadAheadMode(brokerAddr, mode); + } + + @Override + public void createUser(String brokerAddr, + UserInfo userInfo) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + defaultMQAdminExtImpl.createUser(brokerAddr, userInfo); + } + + @Override + public void createUser(String brokerAddr, String username, String password, + String userType) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + defaultMQAdminExtImpl.createUser(brokerAddr, username, password, userType); + } + + @Override + public void updateUser(String brokerAddr, String username, + String password, String userType, + String userStatus) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + defaultMQAdminExtImpl.updateUser(brokerAddr, username, password, userType, userStatus); + } + + @Override + public void updateUser(String brokerAddr, + UserInfo userInfo) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + defaultMQAdminExtImpl.updateUser(brokerAddr, userInfo); + } + + @Override + public void deleteUser(String brokerAddr, + String username) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + defaultMQAdminExtImpl.deleteUser(brokerAddr, username); + } + + @Override + public UserInfo getUser(String brokerAddr, + String username) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + return defaultMQAdminExtImpl.getUser(brokerAddr, username); + } + + @Override + public List listUser(String brokerAddr, + String filter) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + return defaultMQAdminExtImpl.listUser(brokerAddr, filter); + } + + @Override + public void createAcl(String brokerAddr, String subject, List resources, List actions, + List sourceIps, + String decision) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + defaultMQAdminExtImpl.createAcl(brokerAddr, subject, resources, actions, sourceIps, decision); + } + + @Override + public void createAcl(String brokerAddr, + AclInfo aclInfo) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + defaultMQAdminExtImpl.createAcl(brokerAddr, aclInfo); + } + + @Override + public void updateAcl(String brokerAddr, String subject, List resources, List actions, + List sourceIps, + String decision) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + defaultMQAdminExtImpl.updateAcl(brokerAddr, subject, resources, actions, sourceIps, decision); + } + + @Override + public void updateAcl(String brokerAddr, + AclInfo aclInfo) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + defaultMQAdminExtImpl.updateAcl(brokerAddr, aclInfo); + } + + @Override + public void deleteAcl(String brokerAddr, String subject, + String resource) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + defaultMQAdminExtImpl.deleteAcl(brokerAddr, subject, resource); + } + + @Override + public AclInfo getAcl(String brokerAddr, + String subject) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + return defaultMQAdminExtImpl.getAcl(brokerAddr, subject); + } + + @Override + public List listAcl(String brokerAddr, String subjectFilter, + String resourceFilter) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + return defaultMQAdminExtImpl.listAcl(brokerAddr, subjectFilter, resourceFilter); + } + + @Override + public void exportPopRecords(String brokerAddr, long timeout) throws RemotingConnectException, + RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + defaultMQAdminExtImpl.exportPopRecords(brokerAddr, timeout); + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/admin/DefaultMQAdminExtImpl.java b/tools/src/main/java/org/apache/rocketmq/tools/admin/DefaultMQAdminExtImpl.java new file mode 100644 index 0000000..1bdcc76 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/admin/DefaultMQAdminExtImpl.java @@ -0,0 +1,2053 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tools.admin; + +import com.alibaba.fastjson.JSON; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.QueryResult; +import org.apache.rocketmq.client.admin.MQAdminExtInner; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.impl.MQClientManager; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.common.CheckRocksdbCqWriteResult; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.ServiceState; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.common.help.FAQUrl; +import org.apache.rocketmq.common.message.MessageClientExt; +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.message.MessageRequestMode; +import org.apache.rocketmq.common.namesrv.NamesrvUtil; +import org.apache.rocketmq.common.topic.TopicValidator; +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.RPCHook; +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.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.RollbackStats; +import org.apache.rocketmq.remoting.protocol.admin.TopicOffset; +import org.apache.rocketmq.remoting.protocol.admin.TopicStatsTable; +import org.apache.rocketmq.remoting.protocol.body.AclInfo; +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.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.GroupList; +import org.apache.rocketmq.remoting.protocol.body.HARuntimeInfo; +import org.apache.rocketmq.remoting.protocol.body.KVTable; +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.QueueTimeSpan; +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.UserInfo; +import org.apache.rocketmq.remoting.protocol.header.ExportRocksDBConfigToJsonRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateGroupForbiddenRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetMetaDataResponseHeader; +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.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.tools.admin.api.BrokerOperatorResult; +import org.apache.rocketmq.tools.admin.api.MessageTrack; +import org.apache.rocketmq.tools.admin.api.TrackType; +import org.apache.rocketmq.tools.admin.common.AdminToolHandler; +import org.apache.rocketmq.tools.admin.common.AdminToolResult; +import org.apache.rocketmq.tools.admin.common.AdminToolsResultCodeEnum; +import org.apache.rocketmq.tools.command.CommandUtil; + +import java.io.UnsupportedEncodingException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +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.Map.Entry; +import java.util.Properties; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class DefaultMQAdminExtImpl implements MQAdminExt, MQAdminExtInner { + + private static final String SOCKS_PROXY_JSON = "socksProxyJson"; + + private final Logger logger = LoggerFactory.getLogger(DefaultMQAdminExtImpl.class); + private final DefaultMQAdminExt defaultMQAdminExt; + private ServiceState serviceState = ServiceState.CREATE_JUST; + private MQClientInstance mqClientInstance; + private RPCHook rpcHook; + private long timeoutMillis = 20000; + private Random random = new Random(); + + protected final List kvNamespaceToDeleteList = Arrays.asList(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG); + protected ThreadPoolExecutor threadPoolExecutor; + + public DefaultMQAdminExtImpl(DefaultMQAdminExt defaultMQAdminExt, long timeoutMillis) { + this(defaultMQAdminExt, null, timeoutMillis); + } + + public DefaultMQAdminExtImpl(DefaultMQAdminExt defaultMQAdminExt, RPCHook rpcHook, long timeoutMillis) { + this.defaultMQAdminExt = defaultMQAdminExt; + this.rpcHook = rpcHook; + this.timeoutMillis = timeoutMillis; + } + + @Override + public void start() throws MQClientException { + switch (this.serviceState) { + case CREATE_JUST: + this.serviceState = ServiceState.START_FAILED; + + this.defaultMQAdminExt.changeInstanceNameToPID(); + + if ("{}".equals(this.defaultMQAdminExt.getSocksProxyConfig())) { + String proxyConfig = System.getenv(SOCKS_PROXY_JSON); + this.defaultMQAdminExt.setSocksProxyConfig(StringUtils.isNotEmpty(proxyConfig) ? proxyConfig : "{}"); + } + + this.mqClientInstance = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQAdminExt, rpcHook); + + boolean registerOK = mqClientInstance.registerAdminExt(this.defaultMQAdminExt.getAdminExtGroup(), this); + if (!registerOK) { + this.serviceState = ServiceState.CREATE_JUST; + throw new MQClientException("The adminExt group[" + this.defaultMQAdminExt.getAdminExtGroup() + "] has created already, specified another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL), null); + } + + mqClientInstance.start(); + + logger.info("the adminExt [{}] start OK", this.defaultMQAdminExt.getAdminExtGroup()); + + this.serviceState = ServiceState.RUNNING; + + int threadPoolCoreSize = Integer.parseInt(System.getProperty("rocketmq.admin.threadpool.coresize", "20")); + + this.threadPoolExecutor = (ThreadPoolExecutor) ThreadUtils.newThreadPoolExecutor(threadPoolCoreSize, 100, 5, TimeUnit.MINUTES, new LinkedBlockingQueue<>(), new ThreadFactoryImpl("DefaultMQAdminExtImpl_")); + + break; + case RUNNING: + case START_FAILED: + case SHUTDOWN_ALREADY: + throw new MQClientException("The AdminExt service state not OK, maybe started once, " + this.serviceState + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK), null); + default: + break; + } + } + + @Override + public void shutdown() { + switch (this.serviceState) { + case CREATE_JUST: + break; + case RUNNING: + this.mqClientInstance.unregisterAdminExt(this.defaultMQAdminExt.getAdminExtGroup()); + this.mqClientInstance.shutdown(); + + logger.info("the adminExt [{}] shutdown OK", this.defaultMQAdminExt.getAdminExtGroup()); + this.serviceState = ServiceState.SHUTDOWN_ALREADY; + this.threadPoolExecutor.shutdown(); + break; + case SHUTDOWN_ALREADY: + break; + default: + break; + } + } + + @Override + public void addBrokerToContainer(String brokerContainerAddr, + String brokerConfig) throws InterruptedException, MQBrokerException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException { + this.mqClientInstance.getMQClientAPIImpl().addBroker(brokerContainerAddr, brokerConfig, 20000); + } + + @Override + public void removeBrokerFromContainer(String brokerContainerAddr, String clusterName, String brokerName, + long brokerId) throws InterruptedException, MQBrokerException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException { + this.mqClientInstance.getMQClientAPIImpl().removeBroker(brokerContainerAddr, clusterName, brokerName, brokerId, 20000); + } + + public AdminToolResult adminToolExecute(AdminToolHandler handler) { + try { + return handler.doExecute(); + } catch (RemotingException e) { + logger.error("", e); + return AdminToolResult.failure(AdminToolsResultCodeEnum.REMOTING_ERROR, e.getMessage()); + } catch (MQClientException e) { + if (ResponseCode.TOPIC_NOT_EXIST == e.getResponseCode()) { + return AdminToolResult.failure(AdminToolsResultCodeEnum.TOPIC_ROUTE_INFO_NOT_EXIST, e.getErrorMessage()); + } + return AdminToolResult.failure(AdminToolsResultCodeEnum.MQ_CLIENT_ERROR, e.getMessage()); + } catch (InterruptedException e) { + return AdminToolResult.failure(AdminToolsResultCodeEnum.INTERRUPT_ERROR, e.getMessage()); + } catch (Exception e) { + return AdminToolResult.failure(AdminToolsResultCodeEnum.MQ_BROKER_ERROR, e.getMessage()); + } + } + + @Override + public void updateBrokerConfig(String brokerAddr, + Properties properties) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, UnsupportedEncodingException, InterruptedException, MQBrokerException, MQClientException { + this.mqClientInstance.getMQClientAPIImpl().updateBrokerConfig(brokerAddr, properties, timeoutMillis); + } + + @Override + public Properties getBrokerConfig( + final String brokerAddr) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, UnsupportedEncodingException, InterruptedException, MQBrokerException { + return this.mqClientInstance.getMQClientAPIImpl().getBrokerConfig(brokerAddr, timeoutMillis); + } + + @Override + public void createAndUpdateTopicConfig(String addr, + TopicConfig config) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + this.mqClientInstance.getMQClientAPIImpl().createTopic(addr, this.defaultMQAdminExt.getCreateTopicKey(), config, timeoutMillis); + } + + @Override + public void createAndUpdateTopicConfigList(final String brokerAddr, + final List topicConfigList) throws RemotingException, InterruptedException, MQClientException { + this.mqClientInstance.getMQClientAPIImpl().createTopicList(brokerAddr, topicConfigList, timeoutMillis); + } + + @Override + public void createAndUpdateSubscriptionGroupConfig(String addr, + SubscriptionGroupConfig config) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + this.mqClientInstance.getMQClientAPIImpl().createSubscriptionGroup(addr, config, timeoutMillis); + } + + @Override + public void createAndUpdateSubscriptionGroupConfigList(String brokerAddr, + List configs) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + this.mqClientInstance.getMQClientAPIImpl().createSubscriptionGroupList(brokerAddr, configs, timeoutMillis); + } + + @Override + public SubscriptionGroupConfig examineSubscriptionGroupConfig(String addr, + String group) throws InterruptedException, RemotingException, MQClientException, MQBrokerException { + SubscriptionGroupWrapper wrapper = this.mqClientInstance.getMQClientAPIImpl().getAllSubscriptionGroup(addr, timeoutMillis); + return wrapper.getSubscriptionGroupTable().get(group); + } + + @Override + public TopicConfig examineTopicConfig(String addr, + String topic) throws InterruptedException, MQBrokerException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException { + return this.mqClientInstance.getMQClientAPIImpl().getTopicConfig(addr, topic, timeoutMillis); + } + + @Override + public TopicStatsTable examineTopicStats( + String topic) throws RemotingException, MQClientException, InterruptedException, MQBrokerException { + TopicRouteData topicRouteData = this.examineTopicRouteInfo(topic); + TopicStatsTable topicStatsTable = new TopicStatsTable(); + + for (BrokerData bd : topicRouteData.getBrokerDatas()) { + String addr = bd.selectBrokerAddr(); + if (addr != null) { + TopicStatsTable tst = this.mqClientInstance.getMQClientAPIImpl().getTopicStatsInfo(addr, topic, timeoutMillis); + topicStatsTable.getOffsetTable().putAll(tst.getOffsetTable()); + } + } + + //Get the static stats + Map brokerConfigMap = MQAdminUtils.examineTopicConfigFromRoute(topic, topicRouteData, defaultMQAdminExt); + MQAdminUtils.convertPhysicalTopicStats(topic, brokerConfigMap, topicStatsTable); + + if (topicStatsTable.getOffsetTable().isEmpty()) { + throw new MQClientException("Not found the topic stats info", null); + } + + return topicStatsTable; + } + + @Override + public AdminToolResult examineTopicStatsConcurrent(final String topic) { + return adminToolExecute(new AdminToolHandler() { + @Override + public AdminToolResult doExecute() throws Exception { + final TopicStatsTable topicStatsTable = new TopicStatsTable(); + TopicRouteData topicRouteData = examineTopicRouteInfo(topic); + + if (topicRouteData == null || CollectionUtils.isEmpty(topicRouteData.getBrokerDatas())) { + return AdminToolResult.success(topicStatsTable); + } + final CountDownLatch latch = new CountDownLatch(topicRouteData.getBrokerDatas().size()); + for (final BrokerData bd : topicRouteData.getBrokerDatas()) { + threadPoolExecutor.submit(new Runnable() { + @Override + public void run() { + try { + String addr = bd.selectBrokerAddr(); + if (addr != null) { + TopicStatsTable tst = mqClientInstance.getMQClientAPIImpl().getTopicStatsInfo(addr, topic, timeoutMillis); + topicStatsTable.getOffsetTable().putAll(tst.getOffsetTable()); + topicStatsTable.setTopicPutTps(topicStatsTable.getTopicPutTps() + tst.getTopicPutTps()); + } + } catch (Exception e) { + logger.error("getTopicStatsInfo error. topic={}", topic, e); + } finally { + latch.countDown(); + } + } + }); + } + latch.await(timeoutMillis, TimeUnit.MILLISECONDS); + + return AdminToolResult.success(topicStatsTable); + } + }); + } + + @Override + public TopicStatsTable examineTopicStats(String brokerAddr, + String topic) throws RemotingException, MQClientException, InterruptedException, MQBrokerException { + return this.mqClientInstance.getMQClientAPIImpl().getTopicStatsInfo(brokerAddr, topic, timeoutMillis); + } + + @Override + public TopicList fetchAllTopicList() throws RemotingException, MQClientException, InterruptedException { + return this.mqClientInstance.getMQClientAPIImpl().getTopicListFromNameServer(timeoutMillis); + } + + @Override + public TopicList fetchTopicsByCLuster( + String clusterName) throws RemotingException, MQClientException, InterruptedException { + return this.mqClientInstance.getMQClientAPIImpl().getTopicsByCluster(clusterName, timeoutMillis); + } + + @Override + public KVTable fetchBrokerRuntimeStats( + final String brokerAddr) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException { + return this.mqClientInstance.getMQClientAPIImpl().getBrokerRuntimeInfo(brokerAddr, timeoutMillis); + } + + @Override + public ConsumeStats examineConsumeStats( + String consumerGroup, + String topic) throws RemotingException, MQClientException, InterruptedException, MQBrokerException { + return examineConsumeStats(null, consumerGroup, topic); + } + + @Override + public ConsumeStats examineConsumeStats( + String consumerGroup) throws RemotingException, MQClientException, InterruptedException, MQBrokerException { + return examineConsumeStats(null, consumerGroup, null); + } + + @Override + public ConsumeStats examineConsumeStats(String clusterName, String consumerGroup, + String topic) throws RemotingException, MQClientException, InterruptedException, MQBrokerException { + TopicRouteData topicRouteData = null; + List routeTopics = new ArrayList<>(); + routeTopics.add(MixAll.getRetryTopic(consumerGroup)); + if (topic != null) { + routeTopics.add(topic); + routeTopics.add(KeyBuilder.buildPopRetryTopic(topic, consumerGroup)); + } + + // Use clusterName topic to get topic route for lmq or rmq_sys_wheel_timer + if (!StringUtils.isEmpty(topic) && (MixAll.isLmq(topic) || topic.equals(TopicValidator.SYSTEM_TOPIC_PREFIX + "wheel_timer")) && !StringUtils.isEmpty(clusterName)) { + routeTopics.add(clusterName); + } + + for (int i = 0; i < routeTopics.size(); i++) { + try { + topicRouteData = this.examineTopicRouteInfo(routeTopics.get(i)); + if (topicRouteData != null) { + break; + } + } catch (Throwable e) { + if (i == routeTopics.size() - 1) { + throw e; + } + } + } + ConsumeStats result = new ConsumeStats(); + + for (BrokerData bd : topicRouteData.getBrokerDatas()) { + String addr = bd.selectBrokerAddr(); + if (addr != null) { + ConsumeStats consumeStats = this.mqClientInstance.getMQClientAPIImpl().getConsumeStats(addr, consumerGroup, topic, timeoutMillis * 3); + result.getOffsetTable().putAll(consumeStats.getOffsetTable()); + double value = result.getConsumeTps() + consumeStats.getConsumeTps(); + result.setConsumeTps(value); + } + } + + Set topics = new HashSet<>(); + for (MessageQueue messageQueue : result.getOffsetTable().keySet()) { + topics.add(messageQueue.getTopic()); + } + + ConsumeStats staticResult = null; + + if (StringUtils.isEmpty(clusterName)) { + + staticResult = new ConsumeStats(); + staticResult.setConsumeTps(result.getConsumeTps()); + // for topic, we put the physical stats, how about group? + // staticResult.getOffsetTable().putAll(result.getOffsetTable()); + + for (String currentTopic : topics) { + TopicRouteData currentRoute = this.examineTopicRouteInfo(currentTopic); + if (currentRoute.getTopicQueueMappingByBroker() == null + || currentRoute.getTopicQueueMappingByBroker().isEmpty()) { + //normal topic + for (Map.Entry entry : result.getOffsetTable().entrySet()) { + if (entry.getKey().getTopic().equals(currentTopic)) { + staticResult.getOffsetTable().put(entry.getKey(), entry.getValue()); + } + } + } + Map brokerConfigMap = MQAdminUtils.examineTopicConfigFromRoute(currentTopic, currentRoute, defaultMQAdminExt); + ConsumeStats consumeStats = MQAdminUtils.convertPhysicalConsumeStats(brokerConfigMap, result); + staticResult.getOffsetTable().putAll(consumeStats.getOffsetTable()); + } + + } else { + staticResult = result; + } + + if (staticResult.getOffsetTable().isEmpty()) { + ConsumerConnection connection; + try { + connection = this.examineConsumerConnectionInfo(consumerGroup); + } catch (Exception e) { + throw new MQClientException(ResponseCode.CONSUMER_NOT_ONLINE, + "Not found the consumer group consume stats, because return offset table is empty, maybe the consumer not online"); + } + + if (connection.getMessageModel().equals(MessageModel.BROADCASTING)) { + throw new MQClientException(ResponseCode.BROADCAST_CONSUMPTION, + "Not found the consumer group consume stats, because return offset table is empty, the consumer is under the broadcast mode"); + } + } + + return staticResult; + } + + @Override + public ConsumeStats examineConsumeStats(String brokerAddr, String consumerGroup, String topicName, + long timeoutMillis) throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQBrokerException { + return this.mqClientInstance.getMQClientAPIImpl().getConsumeStats(brokerAddr, consumerGroup, topicName, timeoutMillis); + } + + @Override + public AdminToolResult examineConsumeStatsConcurrent(final String consumerGroup, final String topic) { + + return adminToolExecute(new AdminToolHandler() { + @Override + public AdminToolResult doExecute() throws Exception { + TopicRouteData topicRouteData = null; + List routeTopics = new ArrayList<>(); + routeTopics.add(MixAll.getRetryTopic(consumerGroup)); + if (topic != null) { + routeTopics.add(topic); + routeTopics.add(KeyBuilder.buildPopRetryTopic(topic, consumerGroup)); + } + for (int i = 0; i < routeTopics.size(); i++) { + try { + topicRouteData = examineTopicRouteInfo(routeTopics.get(i)); + if (topicRouteData != null) { + break; + } + } catch (Throwable e) { + continue; + } + } + if (topicRouteData == null || CollectionUtils.isEmpty(topicRouteData.getBrokerDatas())) { + return AdminToolResult.failure(AdminToolsResultCodeEnum.TOPIC_ROUTE_INFO_NOT_EXIST, "topic router info not found"); + } + + final ConsumeStats result = new ConsumeStats(); + final CountDownLatch latch = new CountDownLatch(topicRouteData.getBrokerDatas().size()); + final Map consumerTpsMap = new ConcurrentHashMap<>(topicRouteData.getBrokerDatas().size()); + for (final BrokerData bd : topicRouteData.getBrokerDatas()) { + threadPoolExecutor.submit(new Runnable() { + @Override + public void run() { + try { + String addr = bd.selectBrokerAddr(); + if (addr != null) { + ConsumeStats consumeStats = mqClientInstance.getMQClientAPIImpl().getConsumeStats(addr, consumerGroup, topic, timeoutMillis); + result.getOffsetTable().putAll(consumeStats.getOffsetTable()); + consumerTpsMap.put(addr, consumeStats.getConsumeTps()); + } + } catch (Exception e) { + logger.error("getConsumeStats error. topic={}, consumerGroup={}", topic, consumerGroup, e); + } finally { + latch.countDown(); + } + } + }); + } + latch.await(timeoutMillis, TimeUnit.MILLISECONDS); + + for (Double tps : consumerTpsMap.values()) { + result.setConsumeTps(result.getConsumeTps() + tps); + } + + if (result.getOffsetTable().isEmpty()) { + ConsumerConnection connection; + try { + connection = examineConsumerConnectionInfo(consumerGroup); + } catch (Exception e) { + return AdminToolResult.failure(AdminToolsResultCodeEnum.CONSUMER_NOT_ONLINE, "Not found the " + + "consumer group consume stats, because return offset table is empty, maybe the consumer not consume any message"); + } + + if (connection.getMessageModel().equals(MessageModel.BROADCASTING)) { + return AdminToolResult.failure(AdminToolsResultCodeEnum.BROADCAST_CONSUMPTION, "Not found the " + + "consumer group consume stats, because return offset table is empty, the consumer is under the broadcast mode"); + } + } + return AdminToolResult.success(result); + } + }); + } + + @Override + public ClusterInfo examineBrokerClusterInfo() throws InterruptedException, MQBrokerException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException { + return this.mqClientInstance.getMQClientAPIImpl().getBrokerClusterInfo(timeoutMillis); + } + + @Override + public TopicRouteData examineTopicRouteInfo( + String topic) throws RemotingException, MQClientException, InterruptedException { + return this.mqClientInstance.getMQClientAPIImpl().getTopicRouteInfoFromNameServer(topic, timeoutMillis); + } + + @Override + public MessageExt viewMessage(String topic, + String msgId) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + try { + MessageDecoder.decodeMessageId(msgId); + return this.mqClientInstance.getMQAdminImpl().viewMessage(topic, msgId); + } catch (Exception e) { + logger.warn("the msgId maybe created by new client. msgId={}", msgId, e); + } + return this.mqClientInstance.getMQAdminImpl().queryMessageByUniqKey(topic, msgId); + } + + @Override + public MessageExt queryMessage(String clusterName, String topic, + String msgId) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + try { + MessageDecoder.decodeMessageId(msgId); + return this.mqClientInstance.getMQAdminImpl().viewMessage(topic, msgId); + } catch (Exception e) { + logger.warn("the msgId maybe created by new client. msgId={}", msgId, e); + } + return this.mqClientInstance.getMQAdminImpl().queryMessageByUniqKey(clusterName, topic, msgId); + } + + @Override + public ConsumerConnection examineConsumerConnectionInfo( + String consumerGroup) throws InterruptedException, MQBrokerException, + RemotingException, MQClientException { + ConsumerConnection result = new ConsumerConnection(); + String topic = MixAll.getRetryTopic(consumerGroup); + List brokers = this.examineTopicRouteInfo(topic).getBrokerDatas(); + BrokerData brokerData = brokers.get(random.nextInt(brokers.size())); + String addr = null; + if (brokerData != null) { + addr = brokerData.selectBrokerAddr(); + if (StringUtils.isNotBlank(addr)) { + result = this.mqClientInstance.getMQClientAPIImpl().getConsumerConnectionList(addr, consumerGroup, timeoutMillis); + } + } + + if (result.getConnectionSet().isEmpty()) { + logger.warn("the consumer group not online. brokerAddr={}, group={}", addr, consumerGroup); + throw new MQClientException(ResponseCode.CONSUMER_NOT_ONLINE, "Not found the consumer group connection"); + } + + return result; + } + + @Override + public ConsumerConnection examineConsumerConnectionInfo( + String consumerGroup, String brokerAddr) throws InterruptedException, MQBrokerException, + RemotingException, MQClientException { + ConsumerConnection result = + this.mqClientInstance.getMQClientAPIImpl().getConsumerConnectionList(brokerAddr, consumerGroup, timeoutMillis); + + if (result.getConnectionSet().isEmpty()) { + logger.warn("the consumer group not online. brokerAddr={}, group={}", brokerAddr, consumerGroup); + throw new MQClientException(ResponseCode.CONSUMER_NOT_ONLINE, "Not found the consumer group connection"); + } + + return result; + } + + @Override + public ProducerConnection examineProducerConnectionInfo(String producerGroup, + final String topic) throws RemotingException, MQClientException, InterruptedException, MQBrokerException { + ProducerConnection result = new ProducerConnection(); + List brokers = this.examineTopicRouteInfo(topic).getBrokerDatas(); + BrokerData brokerData = brokers.get(random.nextInt(brokers.size())); + String addr = null; + if (brokerData != null) { + addr = brokerData.selectBrokerAddr(); + if (StringUtils.isNotBlank(addr)) { + result = this.mqClientInstance.getMQClientAPIImpl().getProducerConnectionList(addr, producerGroup, timeoutMillis); + } + } + + if (result.getConnectionSet().isEmpty()) { + logger.warn("the producer group not online. brokerAddr={}, group={}", addr, producerGroup); + throw new MQClientException("Not found the producer group connection", null); + } + + return result; + } + + @Override + public ProducerTableInfo getAllProducerInfo( + final String brokerAddr) throws RemotingException, MQClientException, InterruptedException, MQBrokerException { + return this.mqClientInstance.getMQClientAPIImpl().getAllProducerInfo(brokerAddr, timeoutMillis); + } + + @Override + public List getNameServerAddressList() { + return this.mqClientInstance.getMQClientAPIImpl().getNameServerAddressList(); + } + + @Override + public int wipeWritePermOfBroker(final String namesrvAddr, + String brokerName) throws RemotingCommandException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQClientException { + return this.mqClientInstance.getMQClientAPIImpl().wipeWritePermOfBroker(namesrvAddr, brokerName, timeoutMillis); + } + + @Override + public int addWritePermOfBroker(String namesrvAddr, String brokerName) throws RemotingCommandException, + RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQClientException { + return this.mqClientInstance.getMQClientAPIImpl().addWritePermOfBroker(namesrvAddr, brokerName, timeoutMillis); + } + + @Override + public void putKVConfig(String namespace, String key, String value) { + } + + @Override + public String getKVConfig(String namespace, + String key) throws RemotingException, MQClientException, InterruptedException { + return this.mqClientInstance.getMQClientAPIImpl().getKVConfigValue(namespace, key, timeoutMillis); + } + + @Override + public KVTable getKVListByNamespace( + String namespace) throws RemotingException, MQClientException, InterruptedException { + return this.mqClientInstance.getMQClientAPIImpl().getKVListByNamespace(namespace, timeoutMillis); + } + + @Override + public void deleteTopic(String topicName, + String clusterName) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + Set brokerAddressSet = CommandUtil.fetchMasterAndSlaveAddrByClusterName(this.defaultMQAdminExt, clusterName); + this.deleteTopicInBroker(brokerAddressSet, topicName); + List nameServerList = this.getNameServerAddressList(); + Set nameServerSet = new HashSet<>(nameServerList); + this.deleteTopicInNameServer(nameServerSet, topicName); + for (String namespace : this.kvNamespaceToDeleteList) { + this.deleteKvConfig(namespace, topicName); + } + } + + @Override + public void deleteTopicInBroker(Set addrs, + String topic) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + for (String addr : addrs) { + this.mqClientInstance.getMQClientAPIImpl().deleteTopicInBroker(addr, topic, timeoutMillis); + } + } + + @Override + public AdminToolResult deleteTopicInBrokerConcurrent(final Set addrs, + final String topic) { + final List successList = new CopyOnWriteArrayList<>(); + final List failureList = new CopyOnWriteArrayList<>(); + final CountDownLatch latch = new CountDownLatch(addrs.size()); + for (final String addr : addrs) { + threadPoolExecutor.submit(new Runnable() { + @Override + public void run() { + try { + mqClientInstance.getMQClientAPIImpl().deleteTopicInBroker(addr, topic, timeoutMillis); + successList.add(addr); + } catch (Exception e) { + logger.error("deleteTopicInBroker error. topic={}, broker={}", topic, addr, e); + failureList.add(addr); + } finally { + latch.countDown(); + } + } + }); + } + try { + latch.await(timeoutMillis, TimeUnit.MILLISECONDS); + } catch (Exception e) { + } + + BrokerOperatorResult result = new BrokerOperatorResult(); + result.setSuccessList(successList); + result.setFailureList(failureList); + return AdminToolResult.success(result); + } + + @Override + public void deleteTopicInNameServer(Set addrs, + String topic) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + if (addrs == null) { + String ns = this.mqClientInstance.getMQClientAPIImpl().fetchNameServerAddr(); + addrs = new HashSet(Arrays.asList(ns.split(";"))); + } + for (String addr : addrs) { + this.mqClientInstance.getMQClientAPIImpl().deleteTopicInNameServer(addr, topic, timeoutMillis); + } + } + + @Override + public void deleteSubscriptionGroup(String addr, + String groupName) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + this.mqClientInstance.getMQClientAPIImpl().deleteSubscriptionGroup(addr, groupName, false, timeoutMillis); + } + + @Override + public void deleteSubscriptionGroup(String addr, String groupName, + boolean removeOffset) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + this.mqClientInstance.getMQClientAPIImpl().deleteSubscriptionGroup(addr, groupName, removeOffset, timeoutMillis); + } + + @Override + public void createAndUpdateKvConfig(String namespace, String key, + String value) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + this.mqClientInstance.getMQClientAPIImpl().putKVConfigValue(namespace, key, value, timeoutMillis); + } + + @Override + public void deleteKvConfig(String namespace, + String key) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + this.mqClientInstance.getMQClientAPIImpl().deleteKVConfigValue(namespace, key, timeoutMillis); + } + + public List resetOffsetByTimestampOld(String clusterName, String consumerGroup, String topic, + long timestamp, + boolean force) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + String routeTopic = topic; + // Use clusterName topic to get topic route for lmq or rmq_sys_wheel_timer + if (!StringUtils.isEmpty(topic) && (MixAll.isLmq(topic) || topic.equals(TopicValidator.SYSTEM_TOPIC_PREFIX + "wheel_timer")) + && !StringUtils.isEmpty(clusterName)) { + routeTopic = clusterName; + } + TopicRouteData topicRouteData = this.examineTopicRouteInfo(routeTopic); + List rollbackStatsList = new ArrayList<>(); + Map topicRouteMap = new HashMap<>(); + for (QueueData queueData : topicRouteData.getQueueDatas()) { + topicRouteMap.put(queueData.getBrokerName(), queueData); + } + for (BrokerData bd : topicRouteData.getBrokerDatas()) { + String addr = bd.selectBrokerAddr(); + if (addr != null) { + rollbackStatsList.addAll(resetOffsetByTimestampOld(addr, topicRouteMap.get(bd.getBrokerName()), consumerGroup, topic, timestamp, force)); + } + } + return rollbackStatsList; + } + + @Override + public List resetOffsetByTimestampOld(String consumerGroup, String topic, long timestamp, + boolean force) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + return resetOffsetByTimestampOld(null, consumerGroup, topic, timestamp, force); + } + + private List resetOffsetByTimestampOld(String brokerAddr, QueueData queueData, String consumerGroup, + String topic, long timestamp, + boolean force) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + List rollbackStatsList = new ArrayList<>(); + ConsumeStats consumeStats = this.mqClientInstance.getMQClientAPIImpl().getConsumeStats(brokerAddr, consumerGroup, timeoutMillis); + + boolean hasConsumed = false; + for (Map.Entry entry : consumeStats.getOffsetTable().entrySet()) { + MessageQueue queue = entry.getKey(); + OffsetWrapper offsetWrapper = entry.getValue(); + if (topic.equals(queue.getTopic())) { + hasConsumed = true; + RollbackStats rollbackStats = resetOffsetConsumeOffset(brokerAddr, consumerGroup, queue, offsetWrapper, timestamp, force); + rollbackStatsList.add(rollbackStats); + } + } + + if (!hasConsumed) { + Map topicStatus = this.mqClientInstance.getMQClientAPIImpl().getTopicStatsInfo(brokerAddr, topic, timeoutMillis).getOffsetTable(); + for (int i = 0; i < queueData.getReadQueueNums(); i++) { + MessageQueue queue = new MessageQueue(topic, queueData.getBrokerName(), i); + OffsetWrapper offsetWrapper = new OffsetWrapper(); + offsetWrapper.setBrokerOffset(topicStatus.get(queue).getMaxOffset()); + offsetWrapper.setConsumerOffset(topicStatus.get(queue).getMinOffset()); + + RollbackStats rollbackStats = resetOffsetConsumeOffset(brokerAddr, consumerGroup, queue, offsetWrapper, timestamp, force); + rollbackStatsList.add(rollbackStats); + } + } + return rollbackStatsList; + } + + @Override + public Map resetOffsetByTimestamp(String topic, String group, long timestamp, + boolean isForce) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + return resetOffsetByTimestamp(null, topic, group, timestamp, isForce, false); + } + + @Override + public void resetOffsetNew(String consumerGroup, String topic, + long timestamp) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + try { + this.resetOffsetByTimestamp(topic, consumerGroup, timestamp, true); + } catch (MQClientException e) { + if (ResponseCode.CONSUMER_NOT_ONLINE == e.getResponseCode()) { + this.resetOffsetByTimestampOld(consumerGroup, topic, timestamp, true); + return; + } + throw e; + } + } + + @Override + public AdminToolResult resetOffsetNewConcurrent(final String group, final String topic, + final long timestamp) { + return adminToolExecute(new AdminToolHandler() { + @Override + public AdminToolResult doExecute() throws Exception { + TopicRouteData topicRouteData = examineTopicRouteInfo(topic); + if (topicRouteData == null || CollectionUtils.isEmpty(topicRouteData.getBrokerDatas())) { + return AdminToolResult.failure(AdminToolsResultCodeEnum.TOPIC_ROUTE_INFO_NOT_EXIST, "topic router info not found"); + } + final Map topicRouteMap = new HashMap<>(); + for (QueueData queueData : topicRouteData.getQueueDatas()) { + topicRouteMap.put(queueData.getBrokerName(), queueData); + } + + final CopyOnWriteArrayList successList = new CopyOnWriteArrayList(); + final CopyOnWriteArrayList failureList = new CopyOnWriteArrayList(); + final CountDownLatch latch = new CountDownLatch(topicRouteData.getBrokerDatas().size()); + for (final BrokerData bd : topicRouteData.getBrokerDatas()) { + threadPoolExecutor.submit(new Runnable() { + @Override + public void run() { + String addr = bd.selectBrokerAddr(); + try { + if (addr != null) { + Map offsetTable = mqClientInstance.getMQClientAPIImpl().invokeBrokerToResetOffset(addr, topic, group, timestamp, true, timeoutMillis, false); + if (offsetTable != null) { + successList.add(addr); + } else { + failureList.add(addr); + } + } + } catch (MQClientException e) { + if (ResponseCode.CONSUMER_NOT_ONLINE == e.getResponseCode()) { + try { + resetOffsetByTimestampOld(addr, topicRouteMap.get(bd.getBrokerName()), group, topic, timestamp, true); + successList.add(addr); + } catch (Exception e2) { + logger.error(MessageFormat.format("resetOffsetByTimestampOld error. addr={0}, topic={1}, group={2},timestamp={3}", addr, topic, group, timestamp), e); + failureList.add(addr); + } + } else if (ResponseCode.SYSTEM_ERROR == e.getResponseCode()) { + // CODE: 1 DESC: THe consumer group not exist, never online + successList.add(addr); + } else { + failureList.add(addr); + logger.error(MessageFormat.format("resetOffsetNewConcurrent error. addr={0}, topic={1}, group={2},timestamp={3}", addr, topic, group, timestamp), e); + } + } catch (Exception e) { + failureList.add(addr); + logger.error(MessageFormat.format("resetOffsetNewConcurrent error. addr={0}, topic={1}, group={2},timestamp={3}", addr, topic, group, timestamp), e); + } finally { + latch.countDown(); + } + } + }); + } + latch.await(timeoutMillis, TimeUnit.MILLISECONDS); + BrokerOperatorResult result = new BrokerOperatorResult(); + result.setSuccessList(successList); + result.setFailureList(failureList); + if (successList.size() == topicRouteData.getBrokerDatas().size()) { + return AdminToolResult.success(result); + } else { + return AdminToolResult.failure(AdminToolsResultCodeEnum.MQ_BROKER_ERROR, "operator failure", result); + } + } + }); + } + + public Map resetOffsetByTimestamp(String clusterName, String topic, String group, + long timestamp, boolean isForce, + boolean isC) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + String routeTopic = topic; + // Use clusterName topic to get topic route for lmq or rmq_sys_wheel_timer + if (!StringUtils.isEmpty(topic) && (MixAll.isLmq(topic) || topic.equals(TopicValidator.SYSTEM_TOPIC_PREFIX + "wheel_timer")) + && !StringUtils.isEmpty(clusterName)) { + routeTopic = clusterName; + } + TopicRouteData topicRouteData = this.examineTopicRouteInfo(routeTopic); + List brokerDatas = topicRouteData.getBrokerDatas(); + Map allOffsetTable = new HashMap<>(); + if (brokerDatas != null) { + for (BrokerData brokerData : brokerDatas) { + String addr = brokerData.selectBrokerAddr(); + if (addr != null) { + Map offsetTable = this.mqClientInstance.getMQClientAPIImpl().invokeBrokerToResetOffset(addr, topic, group, timestamp, isForce, timeoutMillis, isC); + if (offsetTable != null) { + allOffsetTable.putAll(offsetTable); + } + } + } + } + return allOffsetTable; + } + + private RollbackStats resetOffsetConsumeOffset(String brokerAddr, String consumeGroup, MessageQueue queue, + OffsetWrapper offsetWrapper, long timestamp, + boolean force) throws RemotingException, InterruptedException, MQBrokerException { + long resetOffset; + if (timestamp == -1) { + resetOffset = this.mqClientInstance.getMQClientAPIImpl().getMaxOffset(brokerAddr, queue, timeoutMillis); + } else { + resetOffset = this.mqClientInstance.getMQClientAPIImpl().searchOffset(brokerAddr, queue, timestamp, timeoutMillis); + } + + RollbackStats rollbackStats = new RollbackStats(); + rollbackStats.setBrokerName(queue.getBrokerName()); + rollbackStats.setQueueId(queue.getQueueId()); + rollbackStats.setBrokerOffset(offsetWrapper.getBrokerOffset()); + rollbackStats.setConsumerOffset(offsetWrapper.getConsumerOffset()); + rollbackStats.setTimestampOffset(resetOffset); + rollbackStats.setRollbackOffset(offsetWrapper.getConsumerOffset()); + + if (force || resetOffset <= offsetWrapper.getConsumerOffset()) { + rollbackStats.setRollbackOffset(resetOffset); + UpdateConsumerOffsetRequestHeader requestHeader = new UpdateConsumerOffsetRequestHeader(); + requestHeader.setConsumerGroup(consumeGroup); + requestHeader.setTopic(queue.getTopic()); + requestHeader.setQueueId(queue.getQueueId()); + requestHeader.setCommitOffset(resetOffset); + requestHeader.setBrokerName(queue.getBrokerName()); + this.mqClientInstance.getMQClientAPIImpl().updateConsumerOffset(brokerAddr, requestHeader, timeoutMillis); + } + return rollbackStats; + } + + @Override + public Map> getConsumeStatus(String topic, String group, + String clientAddr) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + TopicRouteData topicRouteData = this.examineTopicRouteInfo(topic); + List brokerDatas = topicRouteData.getBrokerDatas(); + if (brokerDatas != null && brokerDatas.size() > 0) { + String addr = brokerDatas.get(0).selectBrokerAddr(); + if (addr != null) { + return this.mqClientInstance.getMQClientAPIImpl().invokeBrokerToGetConsumerStatus(addr, topic, group, clientAddr, timeoutMillis); + } + } + return Collections.EMPTY_MAP; + } + + @Override + public void createOrUpdateOrderConf(String key, String value, + boolean isCluster) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + + if (isCluster) { + this.mqClientInstance.getMQClientAPIImpl().putKVConfigValue(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG, key, value, timeoutMillis); + } else { + String oldOrderConfs = null; + try { + oldOrderConfs = this.mqClientInstance.getMQClientAPIImpl().getKVConfigValue(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG, key, timeoutMillis); + } catch (Exception e) { + e.printStackTrace(); + } + + Map orderConfMap = new HashMap<>(); + if (!UtilAll.isBlank(oldOrderConfs)) { + String[] oldOrderConfArr = oldOrderConfs.split(";"); + for (String oldOrderConf : oldOrderConfArr) { + String[] items = oldOrderConf.split(":"); + orderConfMap.put(items[0], oldOrderConf); + } + } + String[] items = value.split(":"); + orderConfMap.put(items[0], value); + + StringBuilder newOrderConf = new StringBuilder(); + String splitor = ""; + for (Map.Entry entry : orderConfMap.entrySet()) { + newOrderConf.append(splitor).append(entry.getValue()); + splitor = ";"; + } + this.mqClientInstance.getMQClientAPIImpl().putKVConfigValue(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG, key, newOrderConf.toString(), timeoutMillis); + } + } + + @Override + public GroupList queryTopicConsumeByWho( + String topic) throws InterruptedException, MQBrokerException, RemotingException, MQClientException { + TopicRouteData topicRouteData = this.examineTopicRouteInfo(topic); + + for (BrokerData bd : topicRouteData.getBrokerDatas()) { + String addr = bd.selectBrokerAddr(); + if (addr != null) { + return this.mqClientInstance.getMQClientAPIImpl().queryTopicConsumeByWho(addr, topic, timeoutMillis); + } + } + return null; + } + + @Override + public SubscriptionData querySubscription(String group, + String topic) throws InterruptedException, MQBrokerException, RemotingException, MQClientException { + TopicRouteData topicRouteData = this.examineTopicRouteInfo(topic); + + for (BrokerData bd : topicRouteData.getBrokerDatas()) { + String addr = bd.selectBrokerAddr(); + if (addr != null) { + return this.mqClientInstance.getMQClientAPIImpl().querySubscriptionByConsumer(addr, group, topic, timeoutMillis); + } + } + return null; + } + + @Override + public TopicList queryTopicsByConsumer( + String group) throws InterruptedException, MQBrokerException, RemotingException, MQClientException { + String retryTopic = MixAll.getRetryTopic(group); + TopicRouteData topicRouteData = this.examineTopicRouteInfo(retryTopic); + TopicList result = new TopicList(); + + //Query all brokers + for (BrokerData bd : topicRouteData.getBrokerDatas()) { + String addr = bd.selectBrokerAddr(); + if (addr != null) { + TopicList topicList = this.mqClientInstance.getMQClientAPIImpl().queryTopicsByConsumer(addr, group, timeoutMillis); + result.getTopicList().addAll(topicList.getTopicList()); + } + } + + return result; + } + + @Override + public AdminToolResult queryTopicsByConsumerConcurrent(final String group) { + return adminToolExecute(new AdminToolHandler() { + @Override + public AdminToolResult doExecute() throws Exception { + String retryTopic = MixAll.getRetryTopic(group); + TopicRouteData topicRouteData = examineTopicRouteInfo(retryTopic); + + if (topicRouteData == null || CollectionUtils.isEmpty(topicRouteData.getBrokerDatas())) { + return AdminToolResult.failure(AdminToolsResultCodeEnum.TOPIC_ROUTE_INFO_NOT_EXIST, "router info not found."); + } + final TopicList result = new TopicList(); + final CountDownLatch latch = new CountDownLatch(topicRouteData.getBrokerDatas().size()); + for (final BrokerData bd : topicRouteData.getBrokerDatas()) { + threadPoolExecutor.submit(new Runnable() { + @Override + public void run() { + try { + String addr = bd.selectBrokerAddr(); + if (addr != null) { + TopicList topicList = mqClientInstance.getMQClientAPIImpl().queryTopicsByConsumer(addr, group, timeoutMillis); + result.getTopicList().addAll(topicList.getTopicList()); + } + } catch (Exception e) { + logger.error("queryTopicsByConsumer error. group={}", group, e); + } finally { + latch.countDown(); + } + } + }); + } + latch.await(timeoutMillis, TimeUnit.MILLISECONDS); + + return AdminToolResult.success(result); + } + }); + } + + @Override + public List queryConsumeTimeSpan(final String topic, + final String group) throws InterruptedException, MQBrokerException, RemotingException, MQClientException { + List spanSet = new ArrayList<>(); + TopicRouteData topicRouteData = this.examineTopicRouteInfo(topic); + for (BrokerData bd : topicRouteData.getBrokerDatas()) { + String addr = bd.selectBrokerAddr(); + if (addr != null) { + spanSet.addAll(this.mqClientInstance.getMQClientAPIImpl().queryConsumeTimeSpan(addr, topic, group, timeoutMillis)); + } + } + return spanSet; + } + + @Override + public AdminToolResult> queryConsumeTimeSpanConcurrent(final String topic, final String group) { + return adminToolExecute(new AdminToolHandler() { + @Override + public AdminToolResult doExecute() throws Exception { + final List spanSet = new CopyOnWriteArrayList<>(); + TopicRouteData topicRouteData = examineTopicRouteInfo(topic); + + if (topicRouteData == null || topicRouteData.getBrokerDatas() == null || topicRouteData.getBrokerDatas().size() == 0) { + return AdminToolResult.success(spanSet); + } + final CountDownLatch latch = new CountDownLatch(topicRouteData.getBrokerDatas().size()); + for (final BrokerData bd : topicRouteData.getBrokerDatas()) { + threadPoolExecutor.submit(new Runnable() { + @Override + public void run() { + try { + String addr = bd.selectBrokerAddr(); + if (addr != null) { + spanSet.addAll(mqClientInstance.getMQClientAPIImpl().queryConsumeTimeSpan(addr, topic, group, timeoutMillis)); + } + } catch (Exception e) { + logger.error("queryConsumeTimeSpan error. topic={}, group={}", topic, group, e); + } finally { + latch.countDown(); + } + } + }); + } + latch.await(timeoutMillis, TimeUnit.MILLISECONDS); + + return AdminToolResult.success(spanSet); + } + }); + } + + @Override + public boolean cleanExpiredConsumerQueue( + String cluster) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQClientException, InterruptedException { + boolean result = false; + try { + ClusterInfo clusterInfo = examineBrokerClusterInfo(); + if (null == cluster || "".equals(cluster)) { + for (String targetCluster : clusterInfo.retrieveAllClusterNames()) { + result = cleanExpiredConsumerQueueByCluster(clusterInfo, targetCluster); + } + } else { + result = cleanExpiredConsumerQueueByCluster(clusterInfo, cluster); + } + } catch (MQBrokerException e) { + logger.error("cleanExpiredConsumerQueue error.", e); + } + + return result; + } + + public boolean cleanExpiredConsumerQueueByCluster(ClusterInfo clusterInfo, + String cluster) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQClientException, InterruptedException { + boolean result = false; + String[] addrs = clusterInfo.retrieveAllAddrByCluster(cluster); + for (String addr : addrs) { + result = cleanExpiredConsumerQueueByAddr(addr); + } + return result; + } + + @Override + public boolean cleanExpiredConsumerQueueByAddr( + String addr) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQClientException, InterruptedException { + boolean result = mqClientInstance.getMQClientAPIImpl().cleanExpiredConsumeQueue(addr, timeoutMillis); + logger.warn("clean expired ConsumeQueue on target broker={}, execute result={}", addr, result); + return result; + } + + @Override + public boolean deleteExpiredCommitLog( + String cluster) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQClientException, InterruptedException { + boolean result = false; + try { + ClusterInfo clusterInfo = examineBrokerClusterInfo(); + if (StringUtils.isEmpty(cluster)) { + for (String targetCluster : clusterInfo.retrieveAllClusterNames()) { + result = deleteExpiredCommitLogByCluster(clusterInfo, targetCluster); + } + } else { + result = deleteExpiredCommitLogByCluster(clusterInfo, cluster); + } + } catch (MQBrokerException e) { + logger.error("deleteExpiredCommitLog error.", e); + } + + return result; + } + + public boolean deleteExpiredCommitLogByCluster(ClusterInfo clusterInfo, + String cluster) throws RemotingConnectException, + RemotingSendRequestException, RemotingTimeoutException, MQClientException, InterruptedException { + boolean result = false; + String[] addrs = clusterInfo.retrieveAllAddrByCluster(cluster); + for (String addr : addrs) { + result = deleteExpiredCommitLogByAddr(addr); + } + return result; + } + + @Override + public boolean deleteExpiredCommitLogByAddr( + String addr) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQClientException, InterruptedException { + boolean result = mqClientInstance.getMQClientAPIImpl().deleteExpiredCommitLog(addr, timeoutMillis); + logger.warn("Delete expired CommitLog on target broker={}, execute result={}", addr, result); + return result; + } + + @Override + public boolean cleanUnusedTopic( + String cluster) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQClientException, InterruptedException { + boolean result = false; + try { + ClusterInfo clusterInfo = examineBrokerClusterInfo(); + if (StringUtils.isEmpty(cluster)) { + for (String targetCluster : clusterInfo.retrieveAllClusterNames()) { + result = cleanUnusedTopicByCluster(clusterInfo, targetCluster); + } + } else { + result = cleanUnusedTopicByCluster(clusterInfo, cluster); + } + } catch (MQBrokerException e) { + logger.error("cleanExpiredConsumerQueue error.", e); + } + + return result; + } + + public boolean cleanUnusedTopicByCluster(ClusterInfo clusterInfo, + String cluster) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQClientException, InterruptedException { + boolean result = false; + String[] addrs = clusterInfo.retrieveAllAddrByCluster(cluster); + for (String addr : addrs) { + result = cleanUnusedTopicByAddr(addr); + } + return result; + } + + @Override + public boolean cleanUnusedTopicByAddr( + String addr) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQClientException, InterruptedException { + boolean result = mqClientInstance.getMQClientAPIImpl().cleanUnusedTopicByAddr(addr, timeoutMillis); + logger.warn("clean unused topic on target broker={}, execute result={}", addr, result); + return result; + } + + @Override + public ConsumerRunningInfo getConsumerRunningInfo(String consumerGroup, String clientId, + boolean jstack) throws RemotingException, MQClientException, InterruptedException { + return this.getConsumerRunningInfo(consumerGroup, clientId, jstack, false); + } + + @Override + public ConsumerRunningInfo getConsumerRunningInfo(String consumerGroup, String clientId, boolean jstack, + boolean metrics) throws RemotingException, MQClientException, InterruptedException { + String topic = MixAll.RETRY_GROUP_TOPIC_PREFIX + consumerGroup; + TopicRouteData topicRouteData = this.examineTopicRouteInfo(topic); + List brokerDatas = topicRouteData.getBrokerDatas(); + if (brokerDatas != null) { + for (BrokerData brokerData : brokerDatas) { + String addr = brokerData.selectBrokerAddr(); + if (addr != null) { + return this.mqClientInstance.getMQClientAPIImpl().getConsumerRunningInfo(addr, consumerGroup, clientId, jstack, timeoutMillis); + } + } + } + return null; + } + + @Override + public ConsumeMessageDirectlyResult consumeMessageDirectly(final String consumerGroup, final String clientId, + final String topic, + final String msgId) throws RemotingException, MQClientException, InterruptedException, MQBrokerException { + MessageExt msg = this.viewMessage(topic, msgId); + if (msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX) == null) { + return this.mqClientInstance.getMQClientAPIImpl().consumeMessageDirectly(NetworkUtil.socketAddress2String(msg.getStoreHost()), consumerGroup, clientId, topic, msgId, timeoutMillis); + } else { + MessageClientExt msgClient = (MessageClientExt) msg; + return this.mqClientInstance.getMQClientAPIImpl().consumeMessageDirectly(NetworkUtil.socketAddress2String(msg.getStoreHost()), consumerGroup, clientId, topic, msgClient.getOffsetMsgId(), timeoutMillis); + } + } + + @Override + public ConsumeMessageDirectlyResult consumeMessageDirectly(final String clusterName, final String consumerGroup, + final String clientId, + final String topic, + final String msgId) throws RemotingException, MQClientException, InterruptedException, MQBrokerException { + MessageExt msg = this.queryMessage(clusterName, topic, msgId); + if (msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX) == null) { + return this.mqClientInstance.getMQClientAPIImpl().consumeMessageDirectly(NetworkUtil.socketAddress2String(msg.getStoreHost()), consumerGroup, clientId, topic, msgId, timeoutMillis); + } else { + MessageClientExt msgClient = (MessageClientExt) msg; + return this.mqClientInstance.getMQClientAPIImpl().consumeMessageDirectly(NetworkUtil.socketAddress2String(msg.getStoreHost()), consumerGroup, clientId, topic, msgClient.getOffsetMsgId(), timeoutMillis); + } + } + + @Override + public List messageTrackDetail( + MessageExt msg) throws RemotingException, MQClientException, InterruptedException, MQBrokerException { + List result = new ArrayList<>(); + + GroupList groupList = this.queryTopicConsumeByWho(msg.getTopic()); + + for (String group : groupList.getGroupList()) { + + MessageTrack mt = new MessageTrack(); + mt.setConsumerGroup(group); + mt.setTrackType(TrackType.UNKNOWN); + ConsumerConnection cc = null; + try { + cc = this.examineConsumerConnectionInfo(group); + } catch (MQBrokerException e) { + if (ResponseCode.CONSUMER_NOT_ONLINE == e.getResponseCode()) { + mt.setTrackType(TrackType.NOT_ONLINE); + } + mt.setExceptionDesc("CODE:" + e.getResponseCode() + " DESC:" + e.getErrorMessage()); + result.add(mt); + continue; + } catch (Exception e) { + mt.setExceptionDesc(UtilAll.exceptionSimpleDesc(e)); + result.add(mt); + continue; + } + + switch (cc.getConsumeType()) { + case CONSUME_ACTIVELY: + mt.setTrackType(TrackType.PULL); + break; + case CONSUME_PASSIVELY: + boolean ifConsumed = false; + try { + ifConsumed = this.consumed(msg, group); + } catch (MQClientException e) { + if (ResponseCode.CONSUMER_NOT_ONLINE == e.getResponseCode()) { + mt.setTrackType(TrackType.NOT_ONLINE); + mt.setExceptionDesc("CODE:" + e.getResponseCode() + " DESC:" + e.getErrorMessage()); + } + if (ResponseCode.BROADCAST_CONSUMPTION == e.getResponseCode()) { + mt.setTrackType(TrackType.CONSUME_BROADCASTING); + } + result.add(mt); + continue; + } catch (MQBrokerException e) { + if (ResponseCode.CONSUMER_NOT_ONLINE == e.getResponseCode()) { + mt.setTrackType(TrackType.NOT_ONLINE); + mt.setExceptionDesc("CODE:" + e.getResponseCode() + " DESC:" + e.getErrorMessage()); + } + if (ResponseCode.BROADCAST_CONSUMPTION == e.getResponseCode()) { + mt.setTrackType(TrackType.CONSUME_BROADCASTING); + } + result.add(mt); + continue; + } catch (Exception e) { + mt.setExceptionDesc(UtilAll.exceptionSimpleDesc(e)); + result.add(mt); + continue; + } + + if (ifConsumed) { + mt.setTrackType(TrackType.CONSUMED); + Iterator> it = cc.getSubscriptionTable().entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + if (next.getKey().equals(msg.getTopic())) { + if (next.getValue().getTagsSet().contains(msg.getTags()) || next.getValue().getTagsSet().contains("*") || next.getValue().getTagsSet().isEmpty()) { + } else { + mt.setTrackType(TrackType.CONSUMED_BUT_FILTERED); + } + } + } + } else { + mt.setTrackType(TrackType.NOT_CONSUME_YET); + } + break; + default: + break; + } + result.add(mt); + } + + return result; + } + + @Override + public List messageTrackDetailConcurrent( + final MessageExt msg) throws RemotingException, MQClientException, InterruptedException, MQBrokerException { + final List result = new ArrayList<>(); + + GroupList groupList = this.queryTopicConsumeByWho(msg.getTopic()); + + final CountDownLatch countDownLatch = new CountDownLatch(groupList.getGroupList().size()); + + for (final String group : groupList.getGroupList()) { + + threadPoolExecutor.submit(new Runnable() { + @Override + public void run() { + MessageTrack mt = new MessageTrack(); + mt.setConsumerGroup(group); + mt.setTrackType(TrackType.UNKNOWN); + ConsumerConnection cc = null; + try { + cc = DefaultMQAdminExtImpl.this.examineConsumerConnectionInfo(group); + } catch (MQBrokerException e) { + if (ResponseCode.CONSUMER_NOT_ONLINE == e.getResponseCode()) { + mt.setTrackType(TrackType.NOT_ONLINE); + } + mt.setExceptionDesc("CODE:" + e.getResponseCode() + " DESC:" + e.getErrorMessage()); + result.add(mt); + countDownLatch.countDown(); + return; + } catch (Exception e) { + mt.setExceptionDesc(UtilAll.exceptionSimpleDesc(e)); + result.add(mt); + countDownLatch.countDown(); + return; + } + + switch (cc.getConsumeType()) { + case CONSUME_ACTIVELY: + mt.setTrackType(TrackType.PULL); + break; + case CONSUME_PASSIVELY: + boolean ifConsumed = false; + try { + ifConsumed = DefaultMQAdminExtImpl.this.consumed(msg, group); + } catch (MQClientException e) { + if (ResponseCode.CONSUMER_NOT_ONLINE == e.getResponseCode()) { + mt.setTrackType(TrackType.NOT_ONLINE); + } + mt.setExceptionDesc("CODE:" + e.getResponseCode() + " DESC:" + e.getErrorMessage()); + result.add(mt); + countDownLatch.countDown(); + return; + } catch (MQBrokerException e) { + if (ResponseCode.CONSUMER_NOT_ONLINE == e.getResponseCode()) { + mt.setTrackType(TrackType.NOT_ONLINE); + } + mt.setExceptionDesc("CODE:" + e.getResponseCode() + " DESC:" + e.getErrorMessage()); + result.add(mt); + countDownLatch.countDown(); + return; + } catch (Exception e) { + mt.setExceptionDesc(UtilAll.exceptionSimpleDesc(e)); + result.add(mt); + countDownLatch.countDown(); + return; + } + + if (ifConsumed) { + mt.setTrackType(TrackType.CONSUMED); + Iterator> it = cc.getSubscriptionTable().entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + if (next.getKey().equals(msg.getTopic())) { + if (next.getValue().getTagsSet().contains(msg.getTags()) || next.getValue().getTagsSet().contains("*") || next.getValue().getTagsSet().isEmpty()) { + } else { + mt.setTrackType(TrackType.CONSUMED_BUT_FILTERED); + } + } + } + } else { + mt.setTrackType(TrackType.NOT_CONSUME_YET); + } + break; + default: + break; + } + result.add(mt); + countDownLatch.countDown(); + return; + } + }); + } + + countDownLatch.await(timeoutMillis, TimeUnit.MILLISECONDS); + + return result; + } + + public boolean consumed(final MessageExt msg, + final String group) throws RemotingException, MQClientException, InterruptedException, MQBrokerException { + + ConsumeStats cstats = this.examineConsumeStats(group); + + ClusterInfo ci = this.examineBrokerClusterInfo(); + + Iterator> it = cstats.getOffsetTable().entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + MessageQueue mq = next.getKey(); + if (mq.getTopic().equals(msg.getTopic()) && mq.getQueueId() == msg.getQueueId()) { + BrokerData brokerData = ci.getBrokerAddrTable().get(mq.getBrokerName()); + if (brokerData != null) { + String addr = NetworkUtil.convert2IpString(brokerData.getBrokerAddrs().get(MixAll.MASTER_ID)); + if (NetworkUtil.socketAddress2String(msg.getStoreHost()).equals(addr)) { + if (next.getValue().getConsumerOffset() > msg.getQueueOffset()) { + return true; + } + } + } + } + } + + return false; + } + + public boolean consumedConcurrent(final MessageExt msg, + final String group) throws RemotingException, MQClientException, InterruptedException, MQBrokerException { + + AdminToolResult cstats = this.examineConsumeStatsConcurrent(group, null); + + if (!cstats.isSuccess()) { + throw new MQClientException(cstats.getCode(), cstats.getErrorMsg()); + } + + ClusterInfo ci = this.examineBrokerClusterInfo(); + + if (cstats.isSuccess()) { + for (Entry next : cstats.getData().getOffsetTable().entrySet()) { + MessageQueue mq = next.getKey(); + if (mq.getTopic().equals(msg.getTopic()) && mq.getQueueId() == msg.getQueueId()) { + BrokerData brokerData = ci.getBrokerAddrTable().get(mq.getBrokerName()); + if (brokerData != null) { + String addr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID); + if (addr.equals(NetworkUtil.socketAddress2String(msg.getStoreHost()))) { + if (next.getValue().getConsumerOffset() > msg.getQueueOffset()) { + return true; + } + } + } + } + } + } + + return false; + } + + @Override + public void cloneGroupOffset(String srcGroup, String destGroup, String topic, + boolean isOffline) throws RemotingException, MQClientException, InterruptedException, MQBrokerException { + String retryTopic = MixAll.getRetryTopic(srcGroup); + TopicRouteData topicRouteData = this.examineTopicRouteInfo(retryTopic); + + for (BrokerData bd : topicRouteData.getBrokerDatas()) { + String addr = bd.selectBrokerAddr(); + if (addr != null) { + this.mqClientInstance.getMQClientAPIImpl().cloneGroupOffset(addr, srcGroup, destGroup, topic, isOffline, timeoutMillis); + } + } + } + + @Override + public BrokerStatsData viewBrokerStatsData(String brokerAddr, String statsName, + String statsKey) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQClientException, InterruptedException { + return this.mqClientInstance.getMQClientAPIImpl().viewBrokerStatsData(brokerAddr, statsName, statsKey, timeoutMillis); + } + + @Override + public Set getClusterList( + String topic) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQClientException, InterruptedException { + return this.mqClientInstance.getMQClientAPIImpl().getClusterList(topic, timeoutMillis); + } + + @Override + public ConsumeStatsList fetchConsumeStatsInBroker(final String brokerAddr, boolean isOrder, + long timeoutMillis) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQClientException, InterruptedException { + return this.mqClientInstance.getMQClientAPIImpl().fetchConsumeStatsInBroker(brokerAddr, isOrder, timeoutMillis); + } + + @Override + public Set getTopicClusterList( + final String topic) throws InterruptedException, MQBrokerException, MQClientException, RemotingException { + Set clusterSet = new HashSet<>(); + ClusterInfo clusterInfo = examineBrokerClusterInfo(); + TopicRouteData topicRouteData = examineTopicRouteInfo(topic); + BrokerData brokerData = topicRouteData.getBrokerDatas().get(0); + String brokerName = brokerData.getBrokerName(); + Iterator>> it = clusterInfo.getClusterAddrTable().entrySet().iterator(); + while (it.hasNext()) { + Map.Entry> next = it.next(); + if (next.getValue().contains(brokerName)) { + clusterSet.add(next.getKey()); + } + } + return clusterSet; + } + + @Override + public SubscriptionGroupWrapper getAllSubscriptionGroup(final String brokerAddr, + long timeoutMillis) throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQBrokerException { + return this.mqClientInstance.getMQClientAPIImpl().getAllSubscriptionGroup(brokerAddr, timeoutMillis); + } + + @Override + public SubscriptionGroupWrapper getUserSubscriptionGroup(final String brokerAddr, + long timeoutMillis) throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQBrokerException { + SubscriptionGroupWrapper subscriptionGroupWrapper = this.mqClientInstance.getMQClientAPIImpl().getAllSubscriptionGroup(brokerAddr, timeoutMillis); + + Iterator> iterator = subscriptionGroupWrapper.getSubscriptionGroupTable().entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry configEntry = iterator.next(); + if (MixAll.isSysConsumerGroup(configEntry.getKey()) || MixAll.isPredefinedGroup(configEntry.getKey())) { + iterator.remove(); + } + } + + return subscriptionGroupWrapper; + } + + @Override + public TopicConfigSerializeWrapper getAllTopicConfig(final String brokerAddr, + long timeoutMillis) throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQBrokerException { + return this.mqClientInstance.getMQClientAPIImpl().getAllTopicConfig(brokerAddr, timeoutMillis); + } + + @Override + public TopicConfigSerializeWrapper getUserTopicConfig(final String brokerAddr, final boolean specialTopic, + long timeoutMillis) throws InterruptedException, RemotingException, MQBrokerException, MQClientException { + TopicConfigSerializeWrapper topicConfigSerializeWrapper = this.getAllTopicConfig(brokerAddr, timeoutMillis); + TopicList topicList = this.mqClientInstance.getMQClientAPIImpl().getSystemTopicListFromBroker(brokerAddr, timeoutMillis); + Iterator> iterator = topicConfigSerializeWrapper.getTopicConfigTable().entrySet().iterator(); + while (iterator.hasNext()) { + TopicConfig topicConfig = iterator.next().getValue(); + if (topicList.getTopicList().contains(topicConfig.getTopicName()) + || TopicValidator.isSystemTopic(topicConfig.getTopicName())) { + iterator.remove(); + } else if (!specialTopic && StringUtils.startsWithAny(topicConfig.getTopicName(), + MixAll.RETRY_GROUP_TOPIC_PREFIX, MixAll.DLQ_GROUP_TOPIC_PREFIX)) { + iterator.remove(); + } else if (!PermName.isValid(topicConfig.getPerm())) { + iterator.remove(); + } + } + return topicConfigSerializeWrapper; + } + + @Override + public void createTopic(String key, String newTopic, int queueNum, + Map attributes) throws MQClientException { + createTopic(key, newTopic, queueNum, 0, attributes); + } + + @Override + public void createTopic(String key, String newTopic, int queueNum, int topicSysFlag, + Map attributes) throws MQClientException { + this.mqClientInstance.getMQAdminImpl().createTopic(key, newTopic, queueNum, topicSysFlag, attributes); + } + + @Override + public void createStaticTopic(final String addr, final String defaultTopic, final TopicConfig topicConfig, + final TopicQueueMappingDetail mappingDetail, + final boolean force) throws RemotingException, InterruptedException, MQBrokerException { + this.mqClientInstance.getMQClientAPIImpl().createStaticTopic(addr, defaultTopic, topicConfig, mappingDetail, force, timeoutMillis); + } + + @Override + public long searchOffset(MessageQueue mq, long timestamp) throws MQClientException { + return this.mqClientInstance.getMQAdminImpl().searchOffset(mq, timestamp); + } + + public long searchOffset(MessageQueue mq, long timestamp, BoundaryType boundaryType) throws MQClientException { + return this.mqClientInstance.getMQAdminImpl().searchOffset(mq, timestamp, boundaryType); + } + + @Override + public long maxOffset(MessageQueue mq) throws MQClientException { + return this.mqClientInstance.getMQAdminImpl().maxOffset(mq); + } + + @Override + public long minOffset(MessageQueue mq) throws MQClientException { + return this.mqClientInstance.getMQAdminImpl().minOffset(mq); + } + + @Override + public long earliestMsgStoreTime(MessageQueue mq) throws MQClientException { + return this.mqClientInstance.getMQAdminImpl().earliestMsgStoreTime(mq); + } + + @Override + public QueryResult queryMessage(String topic, String key, int maxNum, long begin, + long end) throws MQClientException, InterruptedException { + + return this.mqClientInstance.getMQAdminImpl().queryMessage(topic, key, maxNum, begin, end); + } + + public QueryResult queryMessage(String clusterName, String topic, String key, int maxNum, long begin, + long end) throws MQClientException, InterruptedException, RemotingException { + return this.mqClientInstance.getMQAdminImpl().queryMessage(clusterName, topic, key, maxNum, begin, end, false); + } + + @Override + public void updateConsumeOffset(String brokerAddr, String consumeGroup, MessageQueue mq, + long offset) throws RemotingException, InterruptedException, MQBrokerException { + UpdateConsumerOffsetRequestHeader requestHeader = new UpdateConsumerOffsetRequestHeader(); + requestHeader.setConsumerGroup(consumeGroup); + requestHeader.setTopic(mq.getTopic()); + requestHeader.setQueueId(mq.getQueueId()); + requestHeader.setCommitOffset(offset); + requestHeader.setBrokerName(mq.getBrokerName()); + this.mqClientInstance.getMQClientAPIImpl().updateConsumerOffset(brokerAddr, requestHeader, timeoutMillis); + } + + @Override + public void updateNameServerConfig(final Properties properties, + final List nameServers) throws InterruptedException, RemotingConnectException, UnsupportedEncodingException, RemotingSendRequestException, RemotingTimeoutException, MQClientException, MQBrokerException { + this.mqClientInstance.getMQClientAPIImpl().updateNameServerConfig(properties, nameServers, timeoutMillis); + } + + @Override + public Map getNameServerConfig( + final List nameServers) throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQClientException, UnsupportedEncodingException { + return this.mqClientInstance.getMQClientAPIImpl().getNameServerConfig(nameServers, timeoutMillis); + } + + @Override + public QueryConsumeQueueResponseBody queryConsumeQueue(String brokerAddr, String topic, int queueId, long index, + int count, + String consumerGroup) throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQClientException { + return this.mqClientInstance.getMQClientAPIImpl().queryConsumeQueue(brokerAddr, topic, queueId, index, count, consumerGroup, timeoutMillis); + } + + @Override + public CheckRocksdbCqWriteResult checkRocksdbCqWriteProgress(String brokerAddr, String topic, long checkStoreTime) + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQClientException { + return this.mqClientInstance.getMQClientAPIImpl().checkRocksdbCqWriteProgress(brokerAddr, topic, checkStoreTime, timeoutMillis); + } + + @Override + public void exportRocksDBConfigToJson(String brokerAddr, + List configType) + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQClientException { + this.mqClientInstance.getMQClientAPIImpl().exportRocksDBConfigToJson(brokerAddr, configType, timeoutMillis); + } + + @Override + public boolean resumeCheckHalfMessage(final String topic, + final String msgId) throws RemotingException, MQClientException, InterruptedException, MQBrokerException { + MessageExt msg = this.viewMessage(topic, msgId); + if (msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX) == null) { + return this.mqClientInstance.getMQClientAPIImpl().resumeCheckHalfMessage(NetworkUtil.socketAddress2String(msg.getStoreHost()), topic, msgId, timeoutMillis); + } else { + MessageClientExt msgClient = (MessageClientExt) msg; + return this.mqClientInstance.getMQClientAPIImpl().resumeCheckHalfMessage(NetworkUtil.socketAddress2String(msg.getStoreHost()), topic, msgClient.getOffsetMsgId(), timeoutMillis); + } + } + + @Override + 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 { + this.mqClientInstance.getMQClientAPIImpl().setMessageRequestMode(brokerAddr, topic, consumerGroup, mode, popShareQueueNum, timeoutMillis); + } + + @Deprecated + @Override + public long searchOffset(final String brokerAddr, final String topicName, final int queueId, final long timestamp, + final long timeoutMillis) throws RemotingException, MQBrokerException, InterruptedException { + return this.mqClientInstance.getMQClientAPIImpl().searchOffset(brokerAddr, topicName, queueId, timestamp, timeoutMillis); + } + + public QueryResult queryMessageByUniqKey(String clusterName, String topic, String key, int maxNum, long begin, + long end) throws MQClientException, InterruptedException { + return this.mqClientInstance.getMQAdminImpl().queryMessageByUniqKey(clusterName, topic, key, maxNum, begin, end); + } + + @Override + public void resetOffsetByQueueId(final String brokerAddr, final String consumeGroup, final String topicName, + final int queueId, final long resetOffset) throws RemotingException, InterruptedException, MQBrokerException { + UpdateConsumerOffsetRequestHeader requestHeader = new UpdateConsumerOffsetRequestHeader(); + requestHeader.setConsumerGroup(consumeGroup); + requestHeader.setTopic(topicName); + requestHeader.setQueueId(queueId); + requestHeader.setCommitOffset(resetOffset); + this.mqClientInstance.getMQClientAPIImpl().updateConsumerOffset(brokerAddr, requestHeader, timeoutMillis); + try { + Map result = mqClientInstance.getMQClientAPIImpl() + .invokeBrokerToResetOffset(brokerAddr, topicName, consumeGroup, 0, queueId, resetOffset, timeoutMillis); + if (null != result) { + for (Map.Entry entry : result.entrySet()) { + logger.info("Reset single message queue {} offset from {} to {}", + JSON.toJSONString(entry.getKey()), entry.getValue(), resetOffset); + } + } + } catch (MQClientException e) { + throw new MQBrokerException(e.getResponseCode(), e.getMessage()); + } + } + + @Override + public Map resetOffsetByTimestamp(String clusterName, String topic, String group, + long timestamp, boolean isForce) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + return resetOffsetByTimestamp(clusterName, topic, group, timestamp, isForce, false); + } + + @Override + public HARuntimeInfo getBrokerHAStatus( + String brokerAddr) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException { + return this.mqClientInstance.getMQClientAPIImpl().getBrokerHAStatus(brokerAddr, timeoutMillis); + } + + @Override + public BrokerReplicasInfo getInSyncStateData(String controllerAddress, + List brokers) throws RemotingException, InterruptedException, MQBrokerException { + return this.mqClientInstance.getMQClientAPIImpl().getInSyncStateData(controllerAddress, brokers); + } + + @Override + public EpochEntryCache getBrokerEpochCache( + String brokerAddr) throws RemotingException, InterruptedException, MQBrokerException { + return this.mqClientInstance.getMQClientAPIImpl().getBrokerEpochCache(brokerAddr); + } + + @Override + public GetMetaDataResponseHeader getControllerMetaData( + String controllerAddr) throws RemotingException, InterruptedException, MQBrokerException { + return this.mqClientInstance.getMQClientAPIImpl().getControllerMetaData(controllerAddr); + } + + @Override + public void resetMasterFlushOffset(String brokerAddr, + long masterFlushOffset) throws InterruptedException, MQBrokerException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException { + this.mqClientInstance.getMQClientAPIImpl().resetMasterFlushOffset(brokerAddr, masterFlushOffset); + } + + @Override + public Pair electMaster(String controllerAddr, String clusterName, + String brokerName, Long brokerId) throws RemotingException, InterruptedException, MQBrokerException { + return this.mqClientInstance.getMQClientAPIImpl().electMaster(controllerAddr, clusterName, brokerName, brokerId); + } + + @Override + public GroupForbidden updateAndGetGroupReadForbidden(String brokerAddr, String groupName, String topicName, + Boolean readable) throws RemotingException, InterruptedException, MQBrokerException { + UpdateGroupForbiddenRequestHeader requestHeader = new UpdateGroupForbiddenRequestHeader(); + requestHeader.setGroup(groupName); + requestHeader.setTopic(topicName); + requestHeader.setReadable(readable); + return this.mqClientInstance.getMQClientAPIImpl().updateAndGetGroupForbidden(brokerAddr, requestHeader, timeoutMillis); + } + + @Override + public void deleteTopicInNameServer(Set addrs, String clusterName, + String topic) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + if (addrs == null) { + String ns = this.mqClientInstance.getMQClientAPIImpl().fetchNameServerAddr(); + addrs = new HashSet(Arrays.asList(ns.split(";"))); + } + for (String addr : addrs) { + this.mqClientInstance.getMQClientAPIImpl().deleteTopicInNameServer(addr, clusterName, topic, timeoutMillis); + } + } + + @Override + public Map getControllerConfig( + List controllerServers) throws InterruptedException, RemotingTimeoutException, + RemotingSendRequestException, RemotingConnectException, MQClientException, + UnsupportedEncodingException { + return this.mqClientInstance.getMQClientAPIImpl().getControllerConfig(controllerServers, timeoutMillis); + } + + @Override + public void updateControllerConfig(Properties properties, + List controllers) throws InterruptedException, RemotingConnectException, UnsupportedEncodingException, + RemotingSendRequestException, RemotingTimeoutException, MQClientException, MQBrokerException { + this.mqClientInstance.getMQClientAPIImpl().updateControllerConfig(properties, controllers, timeoutMillis); + } + + @Override + public void cleanControllerBrokerData(String controllerAddr, String clusterName, String brokerName, + String brokerIdSetToClean, boolean isCleanLivingBroker) + throws RemotingException, InterruptedException, MQBrokerException { + this.mqClientInstance.getMQClientAPIImpl().cleanControllerBrokerData(controllerAddr, clusterName, brokerName, brokerIdSetToClean, isCleanLivingBroker); + } + + public MQClientInstance getMqClientInstance() { + return mqClientInstance; + } + + @Override + public void updateColdDataFlowCtrGroupConfig(String brokerAddr, Properties properties) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, + UnsupportedEncodingException, InterruptedException, MQBrokerException { + this.mqClientInstance.getMQClientAPIImpl().updateColdDataFlowCtrGroupConfig(brokerAddr, properties, timeoutMillis); + } + + @Override + public void removeColdDataFlowCtrGroupConfig(String brokerAddr, String consumerGroup) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, + UnsupportedEncodingException, InterruptedException, MQBrokerException { + this.mqClientInstance.getMQClientAPIImpl().removeColdDataFlowCtrGroupConfig(brokerAddr, consumerGroup, timeoutMillis); + } + + @Override + public String getColdDataFlowCtrInfo(String brokerAddr) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, + UnsupportedEncodingException, InterruptedException, MQBrokerException { + return this.mqClientInstance.getMQClientAPIImpl().getColdDataFlowCtrInfo(brokerAddr, timeoutMillis); + } + + @Override + public String setCommitLogReadAheadMode(String brokerAddr, String mode) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException { + return this.mqClientInstance.getMQClientAPIImpl().setCommitLogReadAheadMode(brokerAddr, mode, timeoutMillis); + } + + @Override + public void createUser(String brokerAddr, + UserInfo userInfo) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + this.mqClientInstance.getMQClientAPIImpl().createUser(brokerAddr, userInfo, timeoutMillis); + } + + @Override + public void createUser(String brokerAddr, String username, String password, + String userType) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + UserInfo userInfo = UserInfo.of(username, password, userType); + this.createUser(brokerAddr, userInfo); + } + + @Override + public void updateUser(String brokerAddr, String username, + String password, String userType, + String userStatus) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + UserInfo userInfo = UserInfo.of(username, password, userType, userStatus); + this.mqClientInstance.getMQClientAPIImpl().updateUser(brokerAddr, userInfo, timeoutMillis); + } + + @Override + public void updateUser(String brokerAddr, + UserInfo userInfo) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + this.mqClientInstance.getMQClientAPIImpl().updateUser(brokerAddr, userInfo, timeoutMillis); + } + + @Override + public void deleteUser(String brokerAddr, + String username) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + this.mqClientInstance.getMQClientAPIImpl().deleteUser(brokerAddr, username, timeoutMillis); + } + + @Override + public UserInfo getUser(String brokerAddr, + String username) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + return this.mqClientInstance.getMQClientAPIImpl().getUser(brokerAddr, username, timeoutMillis); + } + + @Override + public List listUser(String brokerAddr, + String filter) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + return this.mqClientInstance.getMQClientAPIImpl().listUser(brokerAddr, filter, timeoutMillis); + } + + @Override + public void createAcl(String brokerAddr, String subject, List resources, List actions, + List sourceIps, + String decision) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + AclInfo aclInfo = AclInfo.of(subject, resources, actions, sourceIps, decision); + this.createAcl(brokerAddr, aclInfo); + } + + @Override + public void createAcl(String brokerAddr, + AclInfo aclInfo) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + this.mqClientInstance.getMQClientAPIImpl().createAcl(brokerAddr, aclInfo, timeoutMillis); + } + + @Override + public void updateAcl(String brokerAddr, String subject, List resources, List actions, + List sourceIps, + String decision) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + AclInfo aclInfo = AclInfo.of(subject, resources, actions, sourceIps, decision); + this.updateAcl(brokerAddr, aclInfo); + } + + @Override + public void updateAcl(String brokerAddr, + AclInfo aclInfo) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + this.mqClientInstance.getMQClientAPIImpl().updateAcl(brokerAddr, aclInfo, timeoutMillis); + } + + @Override + public void deleteAcl(String brokerAddr, String subject, + String resource) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + this.mqClientInstance.getMQClientAPIImpl().deleteAcl(brokerAddr, subject, resource, timeoutMillis); + } + + @Override + public AclInfo getAcl(String brokerAddr, + String subject) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + return this.mqClientInstance.getMQClientAPIImpl().getAcl(brokerAddr, subject, timeoutMillis); + } + + @Override + public List listAcl(String brokerAddr, String subjectFilter, + String resourceFilter) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + return this.mqClientInstance.getMQClientAPIImpl().listAcl(brokerAddr, subjectFilter, resourceFilter, timeoutMillis); + } + + @Override + public void exportPopRecords(String brokerAddr, long timeout) throws RemotingConnectException, + RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException { + this.mqClientInstance.getMQClientAPIImpl().exportPopRecord(brokerAddr, timeout); + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/admin/MQAdminExt.java b/tools/src/main/java/org/apache/rocketmq/tools/admin/MQAdminExt.java new file mode 100644 index 0000000..a10a589 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/admin/MQAdminExt.java @@ -0,0 +1,517 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tools.admin; + +import org.apache.rocketmq.client.MQAdmin; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.CheckRocksdbCqWriteResult; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.message.MessageRequestMode; +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.protocol.admin.ConsumeStats; +import org.apache.rocketmq.remoting.protocol.admin.RollbackStats; +import org.apache.rocketmq.remoting.protocol.admin.TopicStatsTable; +import org.apache.rocketmq.remoting.protocol.body.AclInfo; +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.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.GroupList; +import org.apache.rocketmq.remoting.protocol.body.HARuntimeInfo; +import org.apache.rocketmq.remoting.protocol.body.KVTable; +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.QueueTimeSpan; +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.UserInfo; +import org.apache.rocketmq.remoting.protocol.header.ExportRocksDBConfigToJsonRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetMetaDataResponseHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +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.tools.admin.api.BrokerOperatorResult; +import org.apache.rocketmq.tools.admin.api.MessageTrack; +import org.apache.rocketmq.tools.admin.common.AdminToolResult; + +import java.io.UnsupportedEncodingException; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +public interface MQAdminExt extends MQAdmin { + void start() throws MQClientException; + + void shutdown(); + + void addBrokerToContainer(final String brokerContainerAddr, final String brokerConfig) throws InterruptedException, + MQBrokerException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException; + + void removeBrokerFromContainer(final String brokerContainerAddr, String clusterName, final String brokerName, + long brokerId) throws InterruptedException, MQBrokerException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException; + + void updateBrokerConfig(final String brokerAddr, final Properties properties) throws RemotingConnectException, + RemotingSendRequestException, RemotingTimeoutException, UnsupportedEncodingException, InterruptedException, MQBrokerException, MQClientException; + + Properties getBrokerConfig(final String brokerAddr) throws RemotingConnectException, + RemotingSendRequestException, RemotingTimeoutException, UnsupportedEncodingException, InterruptedException, MQBrokerException; + + void createAndUpdateTopicConfig(final String addr, + final TopicConfig config) throws RemotingException, MQBrokerException, + InterruptedException, MQClientException; + + void createAndUpdateTopicConfigList(final String addr, + final List topicConfigList) throws InterruptedException, RemotingException, MQClientException; + + void createAndUpdateSubscriptionGroupConfig(final String addr, + final SubscriptionGroupConfig config) throws RemotingException, + MQBrokerException, InterruptedException, MQClientException; + + void createAndUpdateSubscriptionGroupConfigList(String brokerAddr, + List configs) throws RemotingException, + MQBrokerException, InterruptedException, MQClientException; + + SubscriptionGroupConfig examineSubscriptionGroupConfig(final String addr, + final String group) throws InterruptedException, RemotingException, MQClientException, MQBrokerException; + + TopicStatsTable examineTopicStats( + final String topic) throws RemotingException, MQClientException, InterruptedException, + MQBrokerException; + + TopicStatsTable examineTopicStats(String brokerAddr, + final String topic) throws RemotingException, MQClientException, InterruptedException, + MQBrokerException; + + AdminToolResult examineTopicStatsConcurrent(String topic); + + TopicList fetchAllTopicList() throws RemotingException, MQClientException, InterruptedException; + + TopicList fetchTopicsByCLuster( + String clusterName) throws RemotingException, MQClientException, InterruptedException; + + KVTable fetchBrokerRuntimeStats( + final String brokerAddr) throws RemotingConnectException, RemotingSendRequestException, + RemotingTimeoutException, InterruptedException, MQBrokerException; + + ConsumeStats examineConsumeStats( + final String consumerGroup) throws RemotingException, MQClientException, InterruptedException, + MQBrokerException; + + CheckRocksdbCqWriteResult checkRocksdbCqWriteProgress(String brokerAddr, String topic, long checkStoreTime) + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQClientException; + + ConsumeStats examineConsumeStats(final String consumerGroup, + final String topic) throws RemotingException, MQClientException, + InterruptedException, MQBrokerException; + + ConsumeStats examineConsumeStats(final String clusterName, final String consumerGroup, + final String topic) throws RemotingException, MQClientException, + InterruptedException, MQBrokerException; + + ConsumeStats examineConsumeStats(final String brokerAddr, final String consumerGroup, final String topicName, + final long timeoutMillis) throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, + RemotingConnectException, MQBrokerException; + + AdminToolResult examineConsumeStatsConcurrent(String consumerGroup, String topic); + + ClusterInfo examineBrokerClusterInfo() throws InterruptedException, MQBrokerException, RemotingTimeoutException, + RemotingSendRequestException, RemotingConnectException; + + TopicRouteData examineTopicRouteInfo( + final String topic) throws RemotingException, MQClientException, InterruptedException; + + ConsumerConnection examineConsumerConnectionInfo(final String consumerGroup) throws RemotingConnectException, + RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException, RemotingException, + MQClientException; + + ConsumerConnection examineConsumerConnectionInfo( + String consumerGroup, String brokerAddr) throws InterruptedException, MQBrokerException, + RemotingException, MQClientException; + + ProducerConnection examineProducerConnectionInfo(final String producerGroup, + final String topic) throws RemotingException, + MQClientException, InterruptedException, MQBrokerException; + + ProducerTableInfo getAllProducerInfo(final String brokerAddr) throws RemotingException, + MQClientException, InterruptedException, MQBrokerException; + + List getNameServerAddressList(); + + int wipeWritePermOfBroker(final String namesrvAddr, String brokerName) throws RemotingCommandException, + RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQClientException; + + int addWritePermOfBroker(final String namesrvAddr, String brokerName) throws RemotingCommandException, + RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQClientException; + + void putKVConfig(final String namespace, final String key, final String value); + + String getKVConfig(final String namespace, + final String key) throws RemotingException, MQClientException, InterruptedException; + + KVTable getKVListByNamespace( + final String namespace) throws RemotingException, MQClientException, InterruptedException; + + void deleteTopic(final String topicName, + final String clusterName) throws RemotingException, MQBrokerException, InterruptedException, MQClientException; + + void deleteTopicInBroker(final Set addrs, final String topic) throws RemotingException, MQBrokerException, + InterruptedException, MQClientException; + + AdminToolResult deleteTopicInBrokerConcurrent(Set addrs, String topic); + + void deleteTopicInNameServer(final Set addrs, + final String topic) throws RemotingException, MQBrokerException, + InterruptedException, MQClientException; + + void deleteTopicInNameServer(final Set addrs, + final String clusterName, + final String topic) throws RemotingException, MQBrokerException, + InterruptedException, MQClientException; + + void deleteSubscriptionGroup(final String addr, String groupName) throws RemotingException, MQBrokerException, + InterruptedException, MQClientException; + + void deleteSubscriptionGroup(final String addr, String groupName, + boolean removeOffset) throws RemotingException, MQBrokerException, + InterruptedException, MQClientException; + + void createAndUpdateKvConfig(String namespace, String key, + String value) throws RemotingException, MQBrokerException, + InterruptedException, MQClientException; + + void deleteKvConfig(String namespace, String key) throws RemotingException, MQBrokerException, InterruptedException, + MQClientException; + + List resetOffsetByTimestampOld(String consumerGroup, String topic, long timestamp, boolean force) + throws RemotingException, MQBrokerException, InterruptedException, MQClientException; + + Map resetOffsetByTimestamp(String topic, String group, long timestamp, boolean isForce) + throws RemotingException, MQBrokerException, InterruptedException, MQClientException; + + Map resetOffsetByTimestamp(String clusterName, String topic, String group, long timestamp, boolean isForce) + throws RemotingException, MQBrokerException, InterruptedException, MQClientException; + + void resetOffsetNew(String consumerGroup, String topic, long timestamp) throws RemotingException, MQBrokerException, + InterruptedException, MQClientException; + + AdminToolResult resetOffsetNewConcurrent(final String group, final String topic, + final long timestamp); + + Map> getConsumeStatus(String topic, String group, + String clientAddr) throws RemotingException, + MQBrokerException, InterruptedException, MQClientException; + + void createOrUpdateOrderConf(String key, String value, + boolean isCluster) throws RemotingException, MQBrokerException, + InterruptedException, MQClientException; + + GroupList queryTopicConsumeByWho(final String topic) throws RemotingConnectException, RemotingSendRequestException, + RemotingTimeoutException, InterruptedException, MQBrokerException, RemotingException, MQClientException; + + TopicList queryTopicsByConsumer( + final String group) throws InterruptedException, MQBrokerException, RemotingException, MQClientException; + + AdminToolResult queryTopicsByConsumerConcurrent(final String group); + + SubscriptionData querySubscription(final String group, + final String topic) throws InterruptedException, MQBrokerException, RemotingException, MQClientException; + + List queryConsumeTimeSpan(final String topic, + final String group) throws InterruptedException, MQBrokerException, + RemotingException, MQClientException; + + AdminToolResult> queryConsumeTimeSpanConcurrent(final String topic, final String group); + + boolean cleanExpiredConsumerQueue(String cluster) throws RemotingConnectException, RemotingSendRequestException, + RemotingTimeoutException, MQClientException, InterruptedException; + + boolean cleanExpiredConsumerQueueByAddr(String addr) throws RemotingConnectException, RemotingSendRequestException, + RemotingTimeoutException, MQClientException, InterruptedException; + + boolean deleteExpiredCommitLog(String cluster) throws RemotingConnectException, RemotingSendRequestException, + RemotingTimeoutException, MQClientException, InterruptedException; + + boolean deleteExpiredCommitLogByAddr(String addr) throws RemotingConnectException, RemotingSendRequestException, + RemotingTimeoutException, MQClientException, InterruptedException; + + boolean cleanUnusedTopic(String cluster) throws RemotingConnectException, RemotingSendRequestException, + RemotingTimeoutException, MQClientException, InterruptedException; + + boolean cleanUnusedTopicByAddr(String addr) throws RemotingConnectException, RemotingSendRequestException, + RemotingTimeoutException, MQClientException, InterruptedException; + + ConsumerRunningInfo getConsumerRunningInfo(final String consumerGroup, final String clientId, final boolean jstack) + throws RemotingException, MQClientException, InterruptedException; + + ConsumerRunningInfo getConsumerRunningInfo(final String consumerGroup, final String clientId, final boolean jstack, + final boolean metrics) + throws RemotingException, MQClientException, InterruptedException; + + ConsumeMessageDirectlyResult consumeMessageDirectly(String consumerGroup, + String clientId, + String topic, + String msgId) throws RemotingException, MQClientException, InterruptedException, MQBrokerException; + + ConsumeMessageDirectlyResult consumeMessageDirectly(String clusterName, String consumerGroup, + String clientId, + String topic, + String msgId) throws RemotingException, MQClientException, InterruptedException, MQBrokerException; + + List messageTrackDetail( + MessageExt msg) throws RemotingException, MQClientException, InterruptedException, + MQBrokerException; + + List messageTrackDetailConcurrent( + MessageExt msg) throws RemotingException, MQClientException, InterruptedException, + MQBrokerException; + + void cloneGroupOffset(String srcGroup, String destGroup, String topic, boolean isOffline) throws RemotingException, + MQClientException, InterruptedException, MQBrokerException; + + BrokerStatsData viewBrokerStatsData(final String brokerAddr, final String statsName, final String statsKey) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQClientException, + InterruptedException; + + Set getClusterList(final String topic) throws RemotingConnectException, RemotingSendRequestException, + RemotingTimeoutException, MQClientException, InterruptedException; + + ConsumeStatsList fetchConsumeStatsInBroker(final String brokerAddr, boolean isOrder, + long timeoutMillis) throws RemotingConnectException, RemotingSendRequestException, + RemotingTimeoutException, MQClientException, InterruptedException; + + Set getTopicClusterList( + final String topic) throws InterruptedException, MQBrokerException, MQClientException, RemotingException; + + SubscriptionGroupWrapper getAllSubscriptionGroup(final String brokerAddr, + long timeoutMillis) throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, + RemotingConnectException, MQBrokerException; + + SubscriptionGroupWrapper getUserSubscriptionGroup(final String brokerAddr, + long timeoutMillis) throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, + RemotingConnectException, MQBrokerException; + + TopicConfigSerializeWrapper getAllTopicConfig(final String brokerAddr, + long timeoutMillis) throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, + RemotingConnectException, MQBrokerException; + + TopicConfigSerializeWrapper getUserTopicConfig(final String brokerAddr, final boolean specialTopic, + long timeoutMillis) throws InterruptedException, RemotingException, + MQBrokerException, MQClientException; + + void updateConsumeOffset(String brokerAddr, String consumeGroup, MessageQueue mq, + long offset) throws RemotingException, InterruptedException, MQBrokerException; + + /** + * Update name server config. + *
    + * Command Code : RequestCode.UPDATE_NAMESRV_CONFIG + * + *
    If param(nameServers) is null or empty, will use name servers from ns! + */ + void updateNameServerConfig(final Properties properties, + final List nameServers) throws InterruptedException, RemotingConnectException, + UnsupportedEncodingException, RemotingSendRequestException, RemotingTimeoutException, + MQClientException, MQBrokerException; + + /** + * Get name server config. + *
    + * Command Code : RequestCode.GET_NAMESRV_CONFIG + *
    If param(nameServers) is null or empty, will use name servers from ns! + * + * @return The fetched name server config + */ + Map getNameServerConfig(final List nameServers) throws InterruptedException, + RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, + MQClientException, UnsupportedEncodingException; + + /** + * query consume queue data + * + * @param brokerAddr broker ip address + * @param topic topic + * @param queueId id of queue + * @param index start offset + * @param count how many + * @param consumerGroup group + */ + QueryConsumeQueueResponseBody queryConsumeQueue(final String brokerAddr, + final String topic, final int queueId, + final long index, final int count, final String consumerGroup) + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQClientException; + + void exportRocksDBConfigToJson(String brokerAddr, + List configType) + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQClientException; + + boolean resumeCheckHalfMessage(final String topic, + final String msgId) throws RemotingException, MQClientException, InterruptedException, MQBrokerException; + + void setMessageRequestMode(final String brokerAddr, final String topic, final String consumerGroup, + final MessageRequestMode mode, final int popWorkGroupSize, final long timeoutMillis) + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, + RemotingConnectException, MQClientException; + + @Deprecated + long searchOffset(final String brokerAddr, final String topicName, + final int queueId, final long timestamp, final long timeoutMillis) + throws RemotingException, MQBrokerException, InterruptedException; + + void resetOffsetByQueueId(final String brokerAddr, final String consumerGroup, + final String topicName, final int queueId, final long resetOffset) + throws RemotingException, InterruptedException, MQBrokerException; + + TopicConfig examineTopicConfig(final String addr, + final String topic) throws InterruptedException, MQBrokerException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException; + + void createStaticTopic(final String addr, final String defaultTopic, final TopicConfig topicConfig, + final TopicQueueMappingDetail mappingDetail, + final boolean force) throws RemotingException, InterruptedException, MQBrokerException; + + GroupForbidden updateAndGetGroupReadForbidden(String brokerAddr, String groupName, String topicName, + Boolean readable) + throws RemotingException, InterruptedException, MQBrokerException; + + MessageExt queryMessage(String clusterName, + String topic, + String msgId) throws RemotingException, MQBrokerException, InterruptedException, MQClientException; + + HARuntimeInfo getBrokerHAStatus(String brokerAddr) throws RemotingConnectException, RemotingSendRequestException, + RemotingTimeoutException, InterruptedException, MQBrokerException; + + BrokerReplicasInfo getInSyncStateData(String controllerAddress, + List brokers) throws RemotingException, InterruptedException, MQBrokerException; + + EpochEntryCache getBrokerEpochCache( + String brokerAddr) throws RemotingException, InterruptedException, MQBrokerException; + + GetMetaDataResponseHeader getControllerMetaData( + String controllerAddr) throws RemotingException, InterruptedException, MQBrokerException; + + /** + * Reset master flush offset in slave + * + * @param brokerAddr slave broker address + * @param masterFlushOffset master flush offset + */ + void resetMasterFlushOffset(String brokerAddr, long masterFlushOffset) + throws InterruptedException, MQBrokerException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException; + + /** + * Get controller config. + *
    + * Command Code : RequestCode.GET_CONTROLLER_CONFIG + * + * @return The fetched controller config + */ + Map getControllerConfig( + List controllerServers) throws InterruptedException, RemotingTimeoutException, + RemotingSendRequestException, RemotingConnectException, MQClientException, UnsupportedEncodingException; + + /** + * Update controller config. + *
    + * Command Code : RequestCode.UPDATE_CONTROLLER_CONFIG + */ + void updateControllerConfig(final Properties properties, + final List controllers) throws InterruptedException, RemotingConnectException, + UnsupportedEncodingException, RemotingSendRequestException, RemotingTimeoutException, MQClientException, MQBrokerException; + + /** + * manual trigger broker elect master + * + * @param controllerAddr controller address + * @param clusterName cluster name + * @param brokerName broker name + * @param brokerId broker id + * @return + * @throws RemotingException + * @throws InterruptedException + * @throws MQBrokerException + */ + Pair electMaster(String controllerAddr, String clusterName, String brokerName, + Long brokerId) throws RemotingException, InterruptedException, MQBrokerException; + + /** + * clean controller broker meta data + */ + void cleanControllerBrokerData(String controllerAddr, String clusterName, String brokerName, + String brokerControllerIdsToClean, + boolean isCleanLivingBroker) throws RemotingException, InterruptedException, MQBrokerException; + + void updateColdDataFlowCtrGroupConfig(final String brokerAddr, final Properties properties) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, UnsupportedEncodingException, InterruptedException, MQBrokerException; + + void removeColdDataFlowCtrGroupConfig(final String brokerAddr, final String consumerGroup) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, UnsupportedEncodingException, InterruptedException, MQBrokerException; + + String getColdDataFlowCtrInfo(final String brokerAddr) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, UnsupportedEncodingException, InterruptedException, MQBrokerException; + + String setCommitLogReadAheadMode(final String brokerAddr, String mode) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, UnsupportedEncodingException, InterruptedException, MQBrokerException; + + void createUser(String brokerAddr, String username, String password, String userType) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException; + + void createUser(String brokerAddr, UserInfo userInfo) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException; + + void updateUser(String brokerAddr, String username, String password, String userType, String userStatus) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException; + + void updateUser(String brokerAddr, UserInfo userInfo) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException; + + void deleteUser(String brokerAddr, String username) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException; + + UserInfo getUser(String brokerAddr, String username) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException; + + List listUser(String brokerAddr, String filter) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException; + + void createAcl(String brokerAddr, String subject, List resources, List actions, List sourceIps, String decision) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException; + + void createAcl(String brokerAddr, AclInfo aclInfo) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException; + + void updateAcl(String brokerAddr, String subject, List resources, List actions, List sourceIps, String decision) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException; + + void updateAcl(String brokerAddr, AclInfo aclInfo) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException; + + void deleteAcl(String brokerAddr, String subject, String resource) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException; + + AclInfo getAcl(String brokerAddr, String subject) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException; + + List listAcl(String brokerAddr, String subjectFilter, String resourceFilter) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException; + + void exportPopRecords(String brokerAddr, long timeout) throws RemotingConnectException, + RemotingSendRequestException, RemotingTimeoutException, MQBrokerException, InterruptedException; +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/admin/MQAdminUtils.java b/tools/src/main/java/org/apache/rocketmq/tools/admin/MQAdminUtils.java new file mode 100644 index 0000000..a5162cd --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/admin/MQAdminUtils.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.tools.admin; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.message.MessageQueue; +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.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.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +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.TopicQueueMappingOne; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingUtils; +import org.apache.rocketmq.remoting.rpc.ClientMetadata; + +import static org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingUtils.checkAndBuildMappingItems; +import static org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingUtils.getMappingDetailFromConfig; + +public class MQAdminUtils { + + + public static ClientMetadata getBrokerMetadata(DefaultMQAdminExt defaultMQAdminExt) throws InterruptedException, RemotingConnectException, RemotingTimeoutException, RemotingSendRequestException, MQBrokerException { + ClientMetadata clientMetadata = new ClientMetadata(); + refreshClusterInfo(defaultMQAdminExt, clientMetadata); + return clientMetadata; + } + + public static ClientMetadata getBrokerAndTopicMetadata(String topic, DefaultMQAdminExt defaultMQAdminExt) throws InterruptedException, RemotingException, MQBrokerException { + ClientMetadata clientMetadata = new ClientMetadata(); + refreshClusterInfo(defaultMQAdminExt, clientMetadata); + refreshTopicRouteInfo(topic, defaultMQAdminExt, clientMetadata); + return clientMetadata; + } + + public static void refreshClusterInfo(DefaultMQAdminExt defaultMQAdminExt, ClientMetadata clientMetadata) throws InterruptedException, MQBrokerException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException { + ClusterInfo clusterInfo = defaultMQAdminExt.examineBrokerClusterInfo(); + if (clusterInfo == null + || clusterInfo.getClusterAddrTable().isEmpty()) { + throw new RuntimeException("The Cluster info is empty"); + } + clientMetadata.refreshClusterInfo(clusterInfo); + } + + public static void refreshTopicRouteInfo(String topic, DefaultMQAdminExt defaultMQAdminExt, ClientMetadata clientMetadata) throws RemotingException, InterruptedException, MQBrokerException { + TopicRouteData routeData = null; + try { + routeData = defaultMQAdminExt.examineTopicRouteInfo(topic); + } catch (MQClientException exception) { + if (exception.getResponseCode() != ResponseCode.TOPIC_NOT_EXIST) { + throw new MQBrokerException(exception.getResponseCode(), exception.getErrorMessage()); + } + } + if (routeData != null + && !routeData.getQueueDatas().isEmpty()) { + clientMetadata.freshTopicRoute(topic, routeData); + } + } + + public static Set getAllBrokersInSameCluster(Collection brokers, DefaultMQAdminExt defaultMQAdminExt) throws InterruptedException, MQBrokerException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException { + ClusterInfo clusterInfo = defaultMQAdminExt.examineBrokerClusterInfo(); + if (clusterInfo == null + || clusterInfo.getClusterAddrTable().isEmpty()) { + throw new RuntimeException("The Cluster info is empty"); + } + Set allBrokers = new HashSet<>(); + for (String broker: brokers) { + if (allBrokers.contains(broker)) { + continue; + } + for (Set clusterBrokers : clusterInfo.getClusterAddrTable().values()) { + if (clusterBrokers.contains(broker)) { + allBrokers.addAll(clusterBrokers); + break; + } + } + } + return allBrokers; + } + + public static void completeNoTargetBrokers(Map brokerConfigMap, DefaultMQAdminExt defaultMQAdminExt) throws InterruptedException, RemotingConnectException, RemotingTimeoutException, RemotingSendRequestException, MQBrokerException { + TopicConfigAndQueueMapping configMapping = brokerConfigMap.values().iterator().next(); + String topic = configMapping.getTopicName(); + int queueNum = configMapping.getMappingDetail().getTotalQueues(); + long newEpoch = configMapping.getMappingDetail().getEpoch(); + Set allBrokers = getAllBrokersInSameCluster(brokerConfigMap.keySet(), defaultMQAdminExt); + for (String broker: allBrokers) { + if (!brokerConfigMap.containsKey(broker)) { + brokerConfigMap.put(broker, new TopicConfigAndQueueMapping(new TopicConfig(topic, 0, 0), new TopicQueueMappingDetail(topic, queueNum, broker, newEpoch))); + } + } + } + + public static void checkIfMasterAlive(Collection brokers, DefaultMQAdminExt defaultMQAdminExt, ClientMetadata clientMetadata) { + for (String broker : brokers) { + String addr = clientMetadata.findMasterBrokerAddr(broker); + if (addr == null) { + throw new RuntimeException("Can't find addr for broker " + broker); + } + } + } + + public static void updateTopicConfigMappingAll(Map brokerConfigMap, DefaultMQAdminExt defaultMQAdminExt, boolean force) throws Exception { + ClientMetadata clientMetadata = getBrokerMetadata(defaultMQAdminExt); + checkIfMasterAlive(brokerConfigMap.keySet(), defaultMQAdminExt, clientMetadata); + //If some succeed, and others fail, it will cause inconsistent data + for (Map.Entry entry : brokerConfigMap.entrySet()) { + String broker = entry.getKey(); + String addr = clientMetadata.findMasterBrokerAddr(broker); + TopicConfigAndQueueMapping configMapping = entry.getValue(); + defaultMQAdminExt.createStaticTopic(addr, defaultMQAdminExt.getCreateTopicKey(), configMapping, configMapping.getMappingDetail(), force); + } + } + + public static void remappingStaticTopic(String topic, Set brokersToMapIn, Set brokersToMapOut, Map brokerConfigMap, int blockSeqSize, boolean force, DefaultMQAdminExt defaultMQAdminExt) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + + ClientMetadata clientMetadata = MQAdminUtils.getBrokerMetadata(defaultMQAdminExt); + MQAdminUtils.checkIfMasterAlive(brokerConfigMap.keySet(), defaultMQAdminExt, clientMetadata); + // now do the remapping + //Step1: let the new leader can be written without the logicOffset + for (String broker: brokersToMapIn) { + String addr = clientMetadata.findMasterBrokerAddr(broker); + TopicConfigAndQueueMapping configMapping = brokerConfigMap.get(broker); + defaultMQAdminExt.createStaticTopic(addr, defaultMQAdminExt.getCreateTopicKey(), configMapping, configMapping.getMappingDetail(), force); + } + //Step2: forbid to write of old leader + for (String broker: brokersToMapOut) { + String addr = clientMetadata.findMasterBrokerAddr(broker); + TopicConfigAndQueueMapping configMapping = brokerConfigMap.get(broker); + defaultMQAdminExt.createStaticTopic(addr, defaultMQAdminExt.getCreateTopicKey(), configMapping, configMapping.getMappingDetail(), force); + } + //Step3: decide the logic offset + for (String broker: brokersToMapOut) { + String addr = clientMetadata.findMasterBrokerAddr(broker); + TopicStatsTable statsTable = defaultMQAdminExt.examineTopicStats(addr, topic); + TopicConfigAndQueueMapping mapOutConfig = brokerConfigMap.get(broker); + for (Map.Entry> entry : mapOutConfig.getMappingDetail().getHostedQueues().entrySet()) { + List items = entry.getValue(); + Integer globalId = entry.getKey(); + if (items.size() < 2) { + continue; + } + LogicQueueMappingItem newLeader = items.get(items.size() - 1); + LogicQueueMappingItem oldLeader = items.get(items.size() - 2); + if (newLeader.getLogicOffset() > 0) { + continue; + } + TopicOffset topicOffset = statsTable.getOffsetTable().get(new MessageQueue(topic, oldLeader.getBname(), oldLeader.getQueueId())); + if (topicOffset == null) { + throw new RuntimeException("Cannot get the max offset for old leader " + oldLeader); + } + //TO DO check the max offset, will it return -1? + if (topicOffset.getMaxOffset() < oldLeader.getStartOffset()) { + throw new RuntimeException("The max offset is smaller then the start offset " + oldLeader + " " + topicOffset.getMaxOffset()); + } + newLeader.setLogicOffset(TopicQueueMappingUtils.blockSeqRoundUp(oldLeader.computeStaticQueueOffsetStrictly(topicOffset.getMaxOffset()), blockSeqSize)); + TopicConfigAndQueueMapping mapInConfig = brokerConfigMap.get(newLeader.getBname()); + //fresh the new leader + TopicQueueMappingDetail.putMappingInfo(mapInConfig.getMappingDetail(), globalId, items); + } + } + //Step4: write to the new leader with logic offset + for (String broker: brokersToMapIn) { + String addr = clientMetadata.findMasterBrokerAddr(broker); + TopicConfigAndQueueMapping configMapping = brokerConfigMap.get(broker); + defaultMQAdminExt.createStaticTopic(addr, defaultMQAdminExt.getCreateTopicKey(), configMapping, configMapping.getMappingDetail(), force); + } + //Step5: write the non-target brokers + for (String broker: brokerConfigMap.keySet()) { + if (brokersToMapIn.contains(broker) || brokersToMapOut.contains(broker)) { + continue; + } + String addr = clientMetadata.findMasterBrokerAddr(broker); + TopicConfigAndQueueMapping configMapping = brokerConfigMap.get(broker); + defaultMQAdminExt.createStaticTopic(addr, defaultMQAdminExt.getCreateTopicKey(), configMapping, configMapping.getMappingDetail(), force); + } + } + + public static Map examineTopicConfigAll(String topic, DefaultMQAdminExt defaultMQAdminExt) throws RemotingException, InterruptedException, MQBrokerException { + Map brokerConfigMap = new HashMap<>(); + ClientMetadata clientMetadata = new ClientMetadata(); + //check all the brokers + ClusterInfo clusterInfo = defaultMQAdminExt.examineBrokerClusterInfo(); + if (clusterInfo != null + && clusterInfo.getBrokerAddrTable() != null) { + clientMetadata.refreshClusterInfo(clusterInfo); + } + for (String broker : clientMetadata.getBrokerAddrTable().keySet()) { + String addr = clientMetadata.findMasterBrokerAddr(broker); + try { + TopicConfigAndQueueMapping mapping = (TopicConfigAndQueueMapping) defaultMQAdminExt.examineTopicConfig(addr, topic); + //allow the config is null + if (mapping != null) { + if (mapping.getMappingDetail() != null) { + assert mapping.getMappingDetail().getBname().equals(broker); + } + brokerConfigMap.put(broker, mapping); + } + } catch (MQBrokerException exception1) { + if (exception1.getResponseCode() != ResponseCode.TOPIC_NOT_EXIST) { + throw exception1; + } + } + } + return brokerConfigMap; + } + + + public static Map examineTopicConfigFromRoute(String topic, TopicRouteData topicRouteData, DefaultMQAdminExt defaultMQAdminExt) throws RemotingException, InterruptedException, MQBrokerException { + Map brokerConfigMap = new HashMap<>(); + for (BrokerData bd : topicRouteData.getBrokerDatas()) { + String broker = bd.getBrokerName(); + String addr = bd.selectBrokerAddr(); + if (addr == null) { + continue; + } + try { + TopicConfigAndQueueMapping mapping = (TopicConfigAndQueueMapping) defaultMQAdminExt.examineTopicConfig(addr, topic); + //allow the config is null + if (mapping != null) { + if (mapping.getMappingDetail() != null) { + assert mapping.getMappingDetail().getBname().equals(broker); + } + brokerConfigMap.put(broker, mapping); + } + } catch (MQBrokerException exception) { + if (exception.getResponseCode() != ResponseCode.TOPIC_NOT_EXIST) { + throw exception; + } + } + } + return brokerConfigMap; + } + + public static void convertPhysicalTopicStats(String topic, Map brokerConfigMap, TopicStatsTable topicStatsTable) { + Map globalIdMap = checkAndBuildMappingItems(getMappingDetailFromConfig(brokerConfigMap.values()), true, false); + for (Map.Entry entry: globalIdMap.entrySet()) { + Integer qid = entry.getKey(); + TopicQueueMappingOne mappingOne = entry.getValue(); + LogicQueueMappingItem minItem = TopicQueueMappingUtils.findLogicQueueMappingItem(mappingOne.getItems(), 0, true); + LogicQueueMappingItem maxItem = TopicQueueMappingUtils.findLogicQueueMappingItem(mappingOne.getItems(), Long.MAX_VALUE, true); + assert minItem != null && maxItem != null; + TopicOffset minTopicOffset = topicStatsTable.getOffsetTable().get(new MessageQueue(topic, minItem.getBname(), minItem.getQueueId())); + TopicOffset maxTopicOffset = topicStatsTable.getOffsetTable().get(new MessageQueue(topic, maxItem.getBname(), maxItem.getQueueId())); + + if (minTopicOffset == null + || maxTopicOffset == null) { + continue; + } + long min = minItem.computeStaticQueueOffsetLoosely(minTopicOffset.getMinOffset()); + if (min < 0) + min = 0; + long max = maxItem.computeStaticQueueOffsetStrictly(maxTopicOffset.getMaxOffset()); + if (max < 0) + max = 0; + long timestamp = maxTopicOffset.getLastUpdateTimestamp(); + + TopicOffset topicOffset = new TopicOffset(); + topicOffset.setMinOffset(min); + topicOffset.setMaxOffset(max); + topicOffset.setLastUpdateTimestamp(timestamp); + topicStatsTable.getOffsetTable().put(new MessageQueue(topic, TopicQueueMappingUtils.getMockBrokerName(mappingOne.getMappingDetail().getScope()), qid), topicOffset); + } + } + + + public static ConsumeStats convertPhysicalConsumeStats(Map brokerConfigMap, ConsumeStats physicalResult) { + Map globalIdMap = checkAndBuildMappingItems(getMappingDetailFromConfig(brokerConfigMap.values()), true, false); + ConsumeStats result = new ConsumeStats(); + result.setConsumeTps(physicalResult.getConsumeTps()); + for (Map.Entry entry : globalIdMap.entrySet()) { + Integer qid = entry.getKey(); + TopicQueueMappingOne mappingOne = entry.getValue(); + MessageQueue messageQueue = new MessageQueue(mappingOne.getTopic(), TopicQueueMappingUtils.getMockBrokerName(mappingOne.getMappingDetail().getScope()), qid); + OffsetWrapper offsetWrapper = new OffsetWrapper(); + long brokerOffset = -1; + long consumerOffset = -1; + long lastTimestamp = -1; //maybe need to be polished + for (int i = mappingOne.getItems().size() - 1; i >= 0; i--) { + LogicQueueMappingItem item = mappingOne.getItems().get(i); + MessageQueue phyQueue = new MessageQueue(mappingOne.getTopic(), item.getBname(), item.getQueueId()); + OffsetWrapper phyOffsetWrapper = physicalResult.getOffsetTable().get(phyQueue); + if (phyOffsetWrapper == null) { + continue; + } + + if (consumerOffset == -1 + && phyOffsetWrapper.getConsumerOffset() >= 0) { + consumerOffset = phyOffsetWrapper.getConsumerOffset(); + lastTimestamp = phyOffsetWrapper.getLastTimestamp(); + } + if (brokerOffset == -1 + && item.getLogicOffset() >= 0) { + brokerOffset = item.computeStaticQueueOffsetStrictly(phyOffsetWrapper.getBrokerOffset()); + } + if (consumerOffset >= 0 + && brokerOffset >= 0) { + break; + } + } + if (brokerOffset >= 0 + && consumerOffset >= 0) { + offsetWrapper.setBrokerOffset(brokerOffset); + offsetWrapper.setConsumerOffset(consumerOffset); + offsetWrapper.setLastTimestamp(lastTimestamp); + result.getOffsetTable().put(messageQueue, offsetWrapper); + } + } + return result; + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/admin/api/BrokerOperatorResult.java b/tools/src/main/java/org/apache/rocketmq/tools/admin/api/BrokerOperatorResult.java new file mode 100644 index 0000000..5ec04b5 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/admin/api/BrokerOperatorResult.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.tools.admin.api; + +import java.util.List; + +public class BrokerOperatorResult { + + private List successList; + + private List failureList; + + public List getSuccessList() { + return successList; + } + + public void setSuccessList(List successList) { + this.successList = successList; + } + + public List getFailureList() { + return failureList; + } + + public void setFailureList(List failureList) { + this.failureList = failureList; + } + + @Override + public String toString() { + return "BrokerOperatorResult{" + + "successList=" + successList + + ", failureList=" + failureList + + '}'; + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/admin/api/MessageTrack.java b/tools/src/main/java/org/apache/rocketmq/tools/admin/api/MessageTrack.java new file mode 100644 index 0000000..09a13d3 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/admin/api/MessageTrack.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.tools.admin.api; + +public class MessageTrack { + private String consumerGroup; + private TrackType trackType; + private String exceptionDesc; + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public TrackType getTrackType() { + return trackType; + } + + public void setTrackType(TrackType trackType) { + this.trackType = trackType; + } + + public String getExceptionDesc() { + return exceptionDesc; + } + + public void setExceptionDesc(String exceptionDesc) { + this.exceptionDesc = exceptionDesc; + } + + @Override + public String toString() { + return "MessageTrack [consumerGroup=" + consumerGroup + ", trackType=" + trackType + + ", exceptionDesc=" + exceptionDesc + "]"; + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/admin/api/TrackType.java b/tools/src/main/java/org/apache/rocketmq/tools/admin/api/TrackType.java new file mode 100644 index 0000000..7d4445a --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/admin/api/TrackType.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.tools.admin.api; + +public enum TrackType { + CONSUMED, + CONSUMED_BUT_FILTERED, + PULL, + NOT_CONSUME_YET, + NOT_ONLINE, + CONSUME_BROADCASTING, + UNKNOWN +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/admin/common/AdminToolHandler.java b/tools/src/main/java/org/apache/rocketmq/tools/admin/common/AdminToolHandler.java new file mode 100644 index 0000000..1afebeb --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/admin/common/AdminToolHandler.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.tools.admin.common; + +public interface AdminToolHandler { + AdminToolResult doExecute() throws Exception; +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/admin/common/AdminToolResult.java b/tools/src/main/java/org/apache/rocketmq/tools/admin/common/AdminToolResult.java new file mode 100644 index 0000000..21b9652 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/admin/common/AdminToolResult.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.tools.admin.common; + +public class AdminToolResult { + + private boolean success; + private int code; + private String errorMsg; + private T data; + + public AdminToolResult(boolean success, int code, String errorMsg, T data) { + this.success = success; + this.code = code; + this.errorMsg = errorMsg; + this.data = data; + } + + public static AdminToolResult success(Object data) { + return new AdminToolResult(true, AdminToolsResultCodeEnum.SUCCESS.getCode(), "success", data); + } + + public static AdminToolResult failure(AdminToolsResultCodeEnum errorCodeEnum, String errorMsg) { + return new AdminToolResult(false, errorCodeEnum.getCode(), errorMsg, null); + } + + public static AdminToolResult failure(AdminToolsResultCodeEnum errorCodeEnum, String errorMsg, Object data) { + return new AdminToolResult(false, errorCodeEnum.getCode(), errorMsg, data); + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getErrorMsg() { + return errorMsg; + } + + public void setErrorMsg(String errorMsg) { + this.errorMsg = errorMsg; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/admin/common/AdminToolsResultCodeEnum.java b/tools/src/main/java/org/apache/rocketmq/tools/admin/common/AdminToolsResultCodeEnum.java new file mode 100644 index 0000000..275d9e5 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/admin/common/AdminToolsResultCodeEnum.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.tools.admin.common; + +public enum AdminToolsResultCodeEnum { + + /** + * + */ + SUCCESS(200), + + REMOTING_ERROR(-1001), + MQ_BROKER_ERROR(-1002), + MQ_CLIENT_ERROR(-1003), + INTERRUPT_ERROR(-1004), + + TOPIC_ROUTE_INFO_NOT_EXIST(-2001), + CONSUMER_NOT_ONLINE(-2002), + BROADCAST_CONSUMPTION(-2003); + + private int code; + + AdminToolsResultCodeEnum(int code) { + this.code = code; + } + + public int getCode() { + return code; + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/CommandUtil.java b/tools/src/main/java/org/apache/rocketmq/tools/command/CommandUtil.java new file mode 100644 index 0000000..9933415 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/CommandUtil.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.tools.command; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.common.MixAll; +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.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.tools.admin.MQAdminExt; + +public class CommandUtil { + + private static final String ERROR_MESSAGE = "Make sure the specified clusterName exists or the name server connected to is correct."; + + public static final String NO_MASTER_PLACEHOLDER = "NO_MASTER"; + + public static Map/*slave addr*/> fetchMasterAndSlaveDistinguish( + final MQAdminExt adminExt, final String clusterName) + throws InterruptedException, RemotingConnectException, + RemotingTimeoutException, RemotingSendRequestException, + MQBrokerException { + Map> masterAndSlaveMap = new HashMap<>(4); + + ClusterInfo clusterInfoSerializeWrapper = adminExt.examineBrokerClusterInfo(); + Set brokerNameSet = clusterInfoSerializeWrapper.getClusterAddrTable().get(clusterName); + + if (brokerNameSet == null) { + System.out.printf("[error] %s", ERROR_MESSAGE); + return masterAndSlaveMap; + } + + for (String brokerName : brokerNameSet) { + BrokerData brokerData = clusterInfoSerializeWrapper.getBrokerAddrTable().get(brokerName); + + if (brokerData == null || brokerData.getBrokerAddrs() == null) { + continue; + } + + String masterAddr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID); + + if (masterAddr == null) { + masterAndSlaveMap.putIfAbsent(NO_MASTER_PLACEHOLDER, new ArrayList<>()); + } else { + masterAndSlaveMap.put(masterAddr, new ArrayList<>()); + } + + for (Entry brokerAddrEntry : brokerData.getBrokerAddrs().entrySet()) { + if (brokerAddrEntry.getValue() == null || brokerAddrEntry.getKey() == MixAll.MASTER_ID) { + continue; + } + + if (masterAddr == null) { + masterAndSlaveMap.get(NO_MASTER_PLACEHOLDER).add(brokerAddrEntry.getValue()); + } else { + masterAndSlaveMap.get(masterAddr).add(brokerAddrEntry.getValue()); + } + } + } + + return masterAndSlaveMap; + } + + public static Set fetchMasterAddrByClusterName(final MQAdminExt adminExt, final String clusterName) + throws InterruptedException, RemotingConnectException, RemotingTimeoutException, + RemotingSendRequestException, MQBrokerException { + Set masterSet = new HashSet<>(); + + ClusterInfo clusterInfoSerializeWrapper = adminExt.examineBrokerClusterInfo(); + + Set brokerNameSet = clusterInfoSerializeWrapper.getClusterAddrTable().get(clusterName); + + if (brokerNameSet != null) { + for (String brokerName : brokerNameSet) { + BrokerData brokerData = clusterInfoSerializeWrapper.getBrokerAddrTable().get(brokerName); + if (brokerData != null) { + + String addr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID); + if (addr != null) { + masterSet.add(addr); + } + } + } + } else { + System.out.printf("[error] %s", ERROR_MESSAGE); + } + + return masterSet; + } + + public static Set fetchMasterAndSlaveAddrByClusterName(final MQAdminExt adminExt, final String clusterName) + throws InterruptedException, RemotingConnectException, RemotingTimeoutException, + RemotingSendRequestException, MQBrokerException { + Set brokerAddressSet = new HashSet<>(); + ClusterInfo clusterInfoSerializeWrapper = adminExt.examineBrokerClusterInfo(); + Set brokerNameSet = clusterInfoSerializeWrapper.getClusterAddrTable().get(clusterName); + if (brokerNameSet != null) { + for (String brokerName : brokerNameSet) { + BrokerData brokerData = clusterInfoSerializeWrapper.getBrokerAddrTable().get(brokerName); + if (brokerData != null) { + final Collection addrs = brokerData.getBrokerAddrs().values(); + brokerAddressSet.addAll(addrs); + } + } + } else { + System.out.printf("[error] %s", ERROR_MESSAGE); + } + + return brokerAddressSet; + } + + public static String fetchMasterAddrByBrokerName(final MQAdminExt adminExt, + final String brokerName) throws Exception { + ClusterInfo clusterInfoSerializeWrapper = adminExt.examineBrokerClusterInfo(); + BrokerData brokerData = clusterInfoSerializeWrapper.getBrokerAddrTable().get(brokerName); + if (null != brokerData) { + String addr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID); + if (addr != null) { + return addr; + } + } + throw new Exception(String.format("No broker address for broker name %s.%n", brokerData)); + } + + public static Set fetchMasterAndSlaveAddrByBrokerName(final MQAdminExt adminExt, final String brokerName) + throws InterruptedException, RemotingConnectException, RemotingTimeoutException, + RemotingSendRequestException, MQBrokerException { + Set brokerAddressSet = new HashSet<>(); + ClusterInfo clusterInfoSerializeWrapper = adminExt.examineBrokerClusterInfo(); + final BrokerData brokerData = clusterInfoSerializeWrapper.getBrokerAddrTable().get(brokerName); + if (brokerData != null) { + brokerAddressSet.addAll(brokerData.getBrokerAddrs().values()); + } + return brokerAddressSet; + } + + public static Set fetchBrokerNameByClusterName(final MQAdminExt adminExt, final String clusterName) + throws Exception { + ClusterInfo clusterInfoSerializeWrapper = adminExt.examineBrokerClusterInfo(); + Set brokerNameSet = clusterInfoSerializeWrapper.getClusterAddrTable().get(clusterName); + if (brokerNameSet == null || brokerNameSet.isEmpty()) { + throw new Exception(ERROR_MESSAGE); + } + return brokerNameSet; + } + + public static String fetchBrokerNameByAddr(final MQAdminExt adminExt, final String addr) throws Exception { + ClusterInfo clusterInfoSerializeWrapper = adminExt.examineBrokerClusterInfo(); + Map brokerAddrTable = clusterInfoSerializeWrapper.getBrokerAddrTable(); + Iterator> it = brokerAddrTable.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + HashMap brokerAddrs = entry.getValue().getBrokerAddrs(); + if (brokerAddrs.containsValue(addr)) { + return entry.getKey(); + } + } + throw new Exception(ERROR_MESSAGE); + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/MQAdminStartup.java b/tools/src/main/java/org/apache/rocketmq/tools/command/MQAdminStartup.java new file mode 100644 index 0000000..b5caf06 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/MQAdminStartup.java @@ -0,0 +1,338 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tools.command; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.acl.common.AclUtils; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.command.auth.CopyAclsSubCommand; +import org.apache.rocketmq.tools.command.auth.CopyUsersSubCommand; +import org.apache.rocketmq.tools.command.auth.CreateAclSubCommand; +import org.apache.rocketmq.tools.command.auth.CreateUserSubCommand; +import org.apache.rocketmq.tools.command.auth.DeleteAclSubCommand; +import org.apache.rocketmq.tools.command.auth.DeleteUserSubCommand; +import org.apache.rocketmq.tools.command.auth.GetAclSubCommand; +import org.apache.rocketmq.tools.command.auth.GetUserSubCommand; +import org.apache.rocketmq.tools.command.auth.ListAclSubCommand; +import org.apache.rocketmq.tools.command.auth.ListUserSubCommand; +import org.apache.rocketmq.tools.command.auth.UpdateAclSubCommand; +import org.apache.rocketmq.tools.command.auth.UpdateUserSubCommand; +import org.apache.rocketmq.tools.command.broker.BrokerConsumeStatsSubCommad; +import org.apache.rocketmq.tools.command.broker.BrokerStatusSubCommand; +import org.apache.rocketmq.tools.command.broker.CleanExpiredCQSubCommand; +import org.apache.rocketmq.tools.command.broker.CleanUnusedTopicCommand; +import org.apache.rocketmq.tools.command.broker.CommitLogSetReadAheadSubCommand; +import org.apache.rocketmq.tools.command.broker.DeleteExpiredCommitLogSubCommand; +import org.apache.rocketmq.tools.command.broker.GetBrokerConfigCommand; +import org.apache.rocketmq.tools.command.broker.GetBrokerEpochSubCommand; +import org.apache.rocketmq.tools.command.broker.GetColdDataFlowCtrInfoSubCommand; +import org.apache.rocketmq.tools.command.broker.RemoveColdDataFlowCtrGroupConfigSubCommand; +import org.apache.rocketmq.tools.command.broker.ResetMasterFlushOffsetSubCommand; +import org.apache.rocketmq.tools.command.broker.SendMsgStatusCommand; +import org.apache.rocketmq.tools.command.broker.UpdateBrokerConfigSubCommand; +import org.apache.rocketmq.tools.command.broker.UpdateColdDataFlowCtrGroupConfigSubCommand; +import org.apache.rocketmq.tools.command.cluster.CLusterSendMsgRTCommand; +import org.apache.rocketmq.tools.command.cluster.ClusterListSubCommand; +import org.apache.rocketmq.tools.command.connection.ConsumerConnectionSubCommand; +import org.apache.rocketmq.tools.command.connection.ProducerConnectionSubCommand; +import org.apache.rocketmq.tools.command.consumer.ConsumerProgressSubCommand; +import org.apache.rocketmq.tools.command.consumer.ConsumerStatusSubCommand; +import org.apache.rocketmq.tools.command.consumer.DeleteSubscriptionGroupCommand; +import org.apache.rocketmq.tools.command.consumer.GetConsumerConfigSubCommand; +import org.apache.rocketmq.tools.command.consumer.SetConsumeModeSubCommand; +import org.apache.rocketmq.tools.command.consumer.StartMonitoringSubCommand; +import org.apache.rocketmq.tools.command.consumer.UpdateSubGroupListSubCommand; +import org.apache.rocketmq.tools.command.consumer.UpdateSubGroupSubCommand; +import org.apache.rocketmq.tools.command.container.AddBrokerSubCommand; +import org.apache.rocketmq.tools.command.container.RemoveBrokerSubCommand; +import org.apache.rocketmq.tools.command.controller.CleanControllerBrokerMetaSubCommand; +import org.apache.rocketmq.tools.command.controller.GetControllerConfigSubCommand; +import org.apache.rocketmq.tools.command.controller.GetControllerMetaDataSubCommand; +import org.apache.rocketmq.tools.command.controller.ReElectMasterSubCommand; +import org.apache.rocketmq.tools.command.controller.UpdateControllerConfigSubCommand; +import org.apache.rocketmq.tools.command.export.ExportConfigsCommand; +import org.apache.rocketmq.tools.command.export.ExportMetadataCommand; +import org.apache.rocketmq.tools.command.export.ExportMetadataInRocksDBCommand; +import org.apache.rocketmq.tools.command.export.ExportMetricsCommand; +import org.apache.rocketmq.tools.command.export.ExportPopRecordCommand; +import org.apache.rocketmq.tools.command.ha.GetSyncStateSetSubCommand; +import org.apache.rocketmq.tools.command.ha.HAStatusSubCommand; +import org.apache.rocketmq.tools.command.message.CheckMsgSendRTCommand; +import org.apache.rocketmq.tools.command.message.ConsumeMessageCommand; +import org.apache.rocketmq.tools.command.message.DumpCompactionLogCommand; +import org.apache.rocketmq.tools.command.message.PrintMessageByQueueCommand; +import org.apache.rocketmq.tools.command.message.PrintMessageSubCommand; +import org.apache.rocketmq.tools.command.message.QueryMsgByIdSubCommand; +import org.apache.rocketmq.tools.command.message.QueryMsgByKeySubCommand; +import org.apache.rocketmq.tools.command.message.QueryMsgByOffsetSubCommand; +import org.apache.rocketmq.tools.command.message.QueryMsgByUniqueKeySubCommand; +import org.apache.rocketmq.tools.command.message.QueryMsgTraceByIdSubCommand; +import org.apache.rocketmq.tools.command.message.SendMessageCommand; +import org.apache.rocketmq.tools.command.metadata.RocksDBConfigToJsonCommand; +import org.apache.rocketmq.tools.command.namesrv.AddWritePermSubCommand; +import org.apache.rocketmq.tools.command.namesrv.DeleteKvConfigCommand; +import org.apache.rocketmq.tools.command.namesrv.GetNamesrvConfigCommand; +import org.apache.rocketmq.tools.command.namesrv.UpdateKvConfigCommand; +import org.apache.rocketmq.tools.command.namesrv.UpdateNamesrvConfigCommand; +import org.apache.rocketmq.tools.command.namesrv.WipeWritePermSubCommand; +import org.apache.rocketmq.tools.command.offset.CloneGroupOffsetCommand; +import org.apache.rocketmq.tools.command.offset.ResetOffsetByTimeCommand; +import org.apache.rocketmq.tools.command.offset.SkipAccumulationSubCommand; +import org.apache.rocketmq.tools.command.producer.ProducerSubCommand; +import org.apache.rocketmq.tools.command.queue.CheckRocksdbCqWriteProgressCommand; +import org.apache.rocketmq.tools.command.queue.QueryConsumeQueueCommand; +import org.apache.rocketmq.tools.command.stats.StatsAllSubCommand; +import org.apache.rocketmq.tools.command.topic.AllocateMQSubCommand; +import org.apache.rocketmq.tools.command.topic.DeleteTopicSubCommand; +import org.apache.rocketmq.tools.command.topic.RemappingStaticTopicSubCommand; +import org.apache.rocketmq.tools.command.topic.TopicClusterSubCommand; +import org.apache.rocketmq.tools.command.topic.TopicListSubCommand; +import org.apache.rocketmq.tools.command.topic.TopicRouteSubCommand; +import org.apache.rocketmq.tools.command.topic.TopicStatusSubCommand; +import org.apache.rocketmq.tools.command.topic.UpdateOrderConfCommand; +import org.apache.rocketmq.tools.command.topic.UpdateStaticTopicSubCommand; +import org.apache.rocketmq.tools.command.topic.UpdateTopicListSubCommand; +import org.apache.rocketmq.tools.command.topic.UpdateTopicPermSubCommand; +import org.apache.rocketmq.tools.command.topic.UpdateTopicSubCommand; + +import java.util.ArrayList; +import java.util.List; + +public class MQAdminStartup { + protected static final List SUB_COMMANDS = new ArrayList<>(); + + private static final String ROCKETMQ_HOME = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, + System.getenv(MixAll.ROCKETMQ_HOME_ENV)); + + public static void main(String[] args) { + main0(args, null); + } + + public static void main0(String[] args, RPCHook rpcHook) { + System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION)); + + //PackageConflictDetect.detectFastjson(); + + initCommand(); + + try { + switch (args.length) { + case 0: + printHelp(); + break; + case 2: + if (args[0].equals("help")) { + SubCommand cmd = findSubCommand(args[1]); + if (cmd != null) { + Options options = ServerUtil.buildCommandlineOptions(new Options()); + options = cmd.buildCommandlineOptions(options); + if (options != null) { + ServerUtil.printCommandLineHelp("mqadmin " + cmd.commandName(), options); + } + } else { + System.out.printf("The sub command %s not exist.%n", args[1]); + } + break; + } + case 1: + default: + SubCommand cmd = findSubCommand(args[0]); + if (cmd != null) { + String[] subargs = parseSubArgs(args); + + Options options = ServerUtil.buildCommandlineOptions(new Options()); + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, cmd.buildCommandlineOptions(options), + new DefaultParser()); + if (null == commandLine) { + return; + } + + if (commandLine.hasOption('n')) { + String namesrvAddr = commandLine.getOptionValue('n'); + System.setProperty(MixAll.NAMESRV_ADDR_PROPERTY, namesrvAddr); + } + if (rpcHook != null) { + cmd.execute(commandLine, options, rpcHook); + } else { + cmd.execute(commandLine, options, AclUtils.getAclRPCHook(ROCKETMQ_HOME + MixAll.ACL_CONF_TOOLS_FILE)); + } + } else { + System.out.printf("The sub command %s not exist.%n", args[0]); + } + break; + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void initCommand() { + initCommand(new UpdateTopicSubCommand()); + initCommand(new UpdateTopicListSubCommand()); + initCommand(new DeleteTopicSubCommand()); + initCommand(new UpdateSubGroupSubCommand()); + initCommand(new UpdateSubGroupListSubCommand()); + initCommand(new SetConsumeModeSubCommand()); + initCommand(new DeleteSubscriptionGroupCommand()); + initCommand(new UpdateBrokerConfigSubCommand()); + initCommand(new UpdateTopicPermSubCommand()); + + initCommand(new TopicRouteSubCommand()); + initCommand(new TopicStatusSubCommand()); + initCommand(new TopicClusterSubCommand()); + + initCommand(new AddBrokerSubCommand()); + initCommand(new RemoveBrokerSubCommand()); + initCommand(new ResetMasterFlushOffsetSubCommand()); + initCommand(new BrokerStatusSubCommand()); + initCommand(new QueryMsgByIdSubCommand()); + initCommand(new QueryMsgByKeySubCommand()); + initCommand(new QueryMsgByUniqueKeySubCommand()); + initCommand(new QueryMsgByOffsetSubCommand()); + initCommand(new QueryMsgTraceByIdSubCommand()); + + initCommand(new PrintMessageSubCommand()); + initCommand(new PrintMessageByQueueCommand()); + initCommand(new SendMsgStatusCommand()); + initCommand(new BrokerConsumeStatsSubCommad()); + + initCommand(new ProducerConnectionSubCommand()); + initCommand(new ConsumerConnectionSubCommand()); + initCommand(new ConsumerProgressSubCommand()); + initCommand(new ConsumerStatusSubCommand()); + initCommand(new CloneGroupOffsetCommand()); + //for producer + initCommand(new ProducerSubCommand()); + + initCommand(new ClusterListSubCommand()); + initCommand(new TopicListSubCommand()); + + initCommand(new UpdateKvConfigCommand()); + initCommand(new DeleteKvConfigCommand()); + + initCommand(new WipeWritePermSubCommand()); + initCommand(new AddWritePermSubCommand()); + initCommand(new ResetOffsetByTimeCommand()); + initCommand(new SkipAccumulationSubCommand()); + + initCommand(new UpdateOrderConfCommand()); + initCommand(new CleanExpiredCQSubCommand()); + initCommand(new DeleteExpiredCommitLogSubCommand()); + initCommand(new CleanUnusedTopicCommand()); + + initCommand(new StartMonitoringSubCommand()); + initCommand(new StatsAllSubCommand()); + + initCommand(new AllocateMQSubCommand()); + + initCommand(new CheckMsgSendRTCommand()); + initCommand(new CLusterSendMsgRTCommand()); + + initCommand(new GetNamesrvConfigCommand()); + initCommand(new UpdateNamesrvConfigCommand()); + initCommand(new GetBrokerConfigCommand()); + initCommand(new GetConsumerConfigSubCommand()); + + initCommand(new QueryConsumeQueueCommand()); + initCommand(new SendMessageCommand()); + initCommand(new ConsumeMessageCommand()); + + initCommand(new UpdateStaticTopicSubCommand()); + initCommand(new RemappingStaticTopicSubCommand()); + + initCommand(new ExportMetadataCommand()); + initCommand(new ExportConfigsCommand()); + initCommand(new ExportMetricsCommand()); + initCommand(new ExportMetadataInRocksDBCommand()); + initCommand(new ExportPopRecordCommand()); + + initCommand(new HAStatusSubCommand()); + + initCommand(new GetSyncStateSetSubCommand()); + initCommand(new GetBrokerEpochSubCommand()); + initCommand(new GetControllerMetaDataSubCommand()); + + initCommand(new GetControllerConfigSubCommand()); + initCommand(new UpdateControllerConfigSubCommand()); + initCommand(new ReElectMasterSubCommand()); + initCommand(new CleanControllerBrokerMetaSubCommand()); + initCommand(new DumpCompactionLogCommand()); + + initCommand(new GetColdDataFlowCtrInfoSubCommand()); + initCommand(new UpdateColdDataFlowCtrGroupConfigSubCommand()); + initCommand(new RemoveColdDataFlowCtrGroupConfigSubCommand()); + initCommand(new CommitLogSetReadAheadSubCommand()); + + initCommand(new CreateUserSubCommand()); + initCommand(new UpdateUserSubCommand()); + initCommand(new DeleteUserSubCommand()); + initCommand(new GetUserSubCommand()); + initCommand(new ListUserSubCommand()); + initCommand(new CopyUsersSubCommand()); + + initCommand(new CreateAclSubCommand()); + initCommand(new UpdateAclSubCommand()); + initCommand(new DeleteAclSubCommand()); + initCommand(new GetAclSubCommand()); + initCommand(new ListAclSubCommand()); + initCommand(new CopyAclsSubCommand()); + initCommand(new RocksDBConfigToJsonCommand()); + initCommand(new CheckRocksdbCqWriteProgressCommand()); + } + + private static void printHelp() { + System.out.printf("The most commonly used mqadmin commands are:%n"); + + for (SubCommand cmd : SUB_COMMANDS) { + System.out.printf(" %-35s %s%n", cmd.commandName(), cmd.commandDesc()); + } + + System.out.printf("%nSee 'mqadmin help ' for more information on a specific command.%n"); + } + + private static SubCommand findSubCommand(final String name) { + for (SubCommand cmd : SUB_COMMANDS) { + if (cmd.commandName().equalsIgnoreCase(name) || cmd.commandAlias() != null && cmd.commandAlias().equalsIgnoreCase(name)) { + return cmd; + } + } + + return null; + } + + private static String[] parseSubArgs(String[] args) { + if (args.length > 1) { + String[] result = new String[args.length - 1]; + for (int i = 0; i < args.length - 1; i++) { + result[i] = args[i + 1]; + } + return result; + } + return null; + } + + public static void initCommand(SubCommand command) { + SUB_COMMANDS.add(command); + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/SubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/SubCommand.java new file mode 100644 index 0000000..3ed2a92 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/SubCommand.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.tools.command; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; + +public interface SubCommand { + String commandName(); + + default String commandAlias() { + return null; + } + + String commandDesc(); + + Options buildCommandlineOptions(final Options options); + + void execute(final CommandLine commandLine, final Options options, RPCHook rpcHook) throws SubCommandException; +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/SubCommandException.java b/tools/src/main/java/org/apache/rocketmq/tools/command/SubCommandException.java new file mode 100644 index 0000000..d907059 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/SubCommandException.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.tools.command; + +public class SubCommandException extends Exception { + private static final long serialVersionUID = 0L; + + /** + * @param msg Message. + */ + public SubCommandException(String msg) { + super(msg); + } + + public SubCommandException(String format, Object... args) { + super(String.format(format, args)); + } + + /** + * @param msg Message. + * @param cause Cause. + */ + public SubCommandException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/auth/CopyAclsSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/CopyAclsSubCommand.java new file mode 100644 index 0000000..16dd5e0 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/CopyAclsSubCommand.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.tools.command.auth; + +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.AclInfo; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class CopyAclsSubCommand implements SubCommand { + + @Override + public String commandName() { + return "copyAcl"; + } + + @Override + public String commandDesc() { + return "Copy acl to cluster."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + + Option opt = new Option("f", "fromBroker", true, "the source broker that the acls copy from"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("t", "toBroker", true, "the target broker that the acls copy to"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("s", "subjects", true, "the subject list of acl to copy."); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, + RPCHook rpcHook) throws SubCommandException { + + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + if (commandLine.hasOption("f") && commandLine.hasOption("t")) { + String sourceBroker = StringUtils.trim(commandLine.getOptionValue("f")); + String targetBroker = StringUtils.trim(commandLine.getOptionValue("t")); + String subjects = StringUtils.trim(commandLine.getOptionValue('s')); + + defaultMQAdminExt.start(); + + List aclInfos = new ArrayList<>(); + if (StringUtils.isNotBlank(subjects)) { + for (String subject : StringUtils.split(subjects, ",")) { + AclInfo aclInfo = defaultMQAdminExt.getAcl(sourceBroker, subject); + if (aclInfo != null) { + aclInfos.add(aclInfo); + } + } + } else { + aclInfos = defaultMQAdminExt.listAcl(sourceBroker, null, null); + } + + if (CollectionUtils.isEmpty(aclInfos)) { + return; + } + + for (AclInfo aclInfo : aclInfos) { + if (defaultMQAdminExt.getAcl(targetBroker, aclInfo.getSubject()) == null) { + defaultMQAdminExt.createAcl(targetBroker, aclInfo); + } else { + defaultMQAdminExt.updateAcl(targetBroker, aclInfo); + } + System.out.printf("copy acl of %s from %s to %s success.%n", aclInfo.getSubject(), sourceBroker, targetBroker); + } + + return; + } + + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/auth/CopyUsersSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/CopyUsersSubCommand.java new file mode 100644 index 0000000..7f2c224 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/CopyUsersSubCommand.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.tools.command.auth; + +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.UserInfo; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class CopyUsersSubCommand implements SubCommand { + + @Override + public String commandName() { + return "copyUser"; + } + + @Override + public String commandDesc() { + return "Copy user to cluster."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + + Option opt = new Option("f", "fromBroker", true, "the source broker that the users copy from"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("t", "toBroker", true, "the target broker that the users copy to"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("u", "usernames", true, "the username list of user to copy."); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, + RPCHook rpcHook) throws SubCommandException { + + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + if (commandLine.hasOption("f") && commandLine.hasOption("t")) { + String sourceBroker = StringUtils.trim(commandLine.getOptionValue("f")); + String targetBroker = StringUtils.trim(commandLine.getOptionValue("t")); + String usernames = StringUtils.trim(commandLine.getOptionValue('u')); + + defaultMQAdminExt.start(); + + List userInfos = new ArrayList<>(); + if (StringUtils.isNotBlank(usernames)) { + for (String username : StringUtils.split(usernames, ",")) { + UserInfo userInfo = defaultMQAdminExt.getUser(sourceBroker, username); + if (userInfo != null) { + userInfos.add(userInfo); + } + } + } else { + userInfos = defaultMQAdminExt.listUser(sourceBroker, null); + } + + if (CollectionUtils.isEmpty(userInfos)) { + return; + } + + for (UserInfo userInfo : userInfos) { + if (defaultMQAdminExt.getUser(targetBroker, userInfo.getUsername()) == null) { + defaultMQAdminExt.createUser(targetBroker, userInfo); + } else { + defaultMQAdminExt.updateUser(targetBroker, userInfo); + } + System.out.printf("copy user of %s from %s to %s success.%n", userInfo.getUsername(), sourceBroker, targetBroker); + } + + return; + } + + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/auth/CreateAclSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/CreateAclSubCommand.java new file mode 100644 index 0000000..3dcfbcc --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/CreateAclSubCommand.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.tools.command.auth; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionGroup; +import org.apache.commons.cli.Options; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class CreateAclSubCommand implements SubCommand { + + @Override + public String commandName() { + return "createAcl"; + } + + @Override + public String commandDesc() { + return "Create acl to cluster."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + OptionGroup optionGroup = new OptionGroup(); + + Option opt = new Option("c", "clusterName", true, "create acl to which cluster"); + optionGroup.addOption(opt); + + opt = new Option("b", "brokerAddr", true, "create acl to which broker"); + optionGroup.addOption(opt); + + optionGroup.setRequired(true); + options.addOptionGroup(optionGroup); + + opt = new Option("s", "subject", true, "the subject of acl to create."); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("r", "resources", true, "the resources of acl to create"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("a", "actions", true, "the actions of acl to create"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("i", "sourceIp", true, "the sourceIps of acl to create"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("d", "decision", true, "the decision of acl to create"); + opt.setRequired(true); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, + RPCHook rpcHook) throws SubCommandException { + + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + String subject = null; + if (commandLine.hasOption('s')) { + subject = StringUtils.trim(commandLine.getOptionValue('s')); + } + List resources = null; + if (commandLine.hasOption('r')) { + resources = Arrays.stream(StringUtils.split(commandLine.getOptionValue('r'), ',')) + .map(StringUtils::trim).collect(Collectors.toList()); + } + List actions = null; + if (commandLine.hasOption('a')) { + actions = Arrays.stream(StringUtils.split(commandLine.getOptionValue('a'), ',')) + .map(StringUtils::trim).collect(Collectors.toList()); + } + + List sourceIps = null; + if (commandLine.hasOption('i')) { + sourceIps = Arrays.stream(StringUtils.split(commandLine.getOptionValue('i'), ',')) + .map(StringUtils::trim).collect(Collectors.toList()); + } + + String decision = null; + if (commandLine.hasOption('d')) { + decision = StringUtils.trim(commandLine.getOptionValue('d')); + } + + if (commandLine.hasOption('b')) { + String addr = StringUtils.trim(commandLine.getOptionValue('b')); + + defaultMQAdminExt.start(); + defaultMQAdminExt.createAcl(addr, subject, resources, actions, sourceIps, decision); + + System.out.printf("create acl to %s success.%n", addr); + return; + } else if (commandLine.hasOption('c')) { + String clusterName = StringUtils.trim(commandLine.getOptionValue('c')); + + defaultMQAdminExt.start(); + Set brokerAddrSet = + CommandUtil.fetchMasterAndSlaveAddrByClusterName(defaultMQAdminExt, clusterName); + for (String addr : brokerAddrSet) { + defaultMQAdminExt.createAcl(addr, subject, resources, actions, sourceIps, decision); + System.out.printf("create acl to %s success.%n", addr); + } + return; + } + + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/auth/CreateUserSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/CreateUserSubCommand.java new file mode 100644 index 0000000..7dad3d6 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/CreateUserSubCommand.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.tools.command.auth; + +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionGroup; +import org.apache.commons.cli.Options; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class CreateUserSubCommand implements SubCommand { + + @Override + public String commandName() { + return "createUser"; + } + + @Override + public String commandDesc() { + return "Create user to cluster."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + OptionGroup optionGroup = new OptionGroup(); + + Option opt = new Option("c", "clusterName", true, "create user to which cluster"); + optionGroup.addOption(opt); + + opt = new Option("b", "brokerAddr", true, "create user to which broker"); + optionGroup.addOption(opt); + + optionGroup.setRequired(true); + options.addOptionGroup(optionGroup); + + opt = new Option("u", "username", true, "the username of user to create."); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("p", "password", true, "the password of user to create"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("t", "userType", true, "the userType of user to create"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, + RPCHook rpcHook) throws SubCommandException { + + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + String username = StringUtils.trim(commandLine.getOptionValue('u')); + String password = StringUtils.trim(commandLine.getOptionValue('p')); + String userType = StringUtils.trim(commandLine.getOptionValue('t')); + + if (commandLine.hasOption('b')) { + String addr = StringUtils.trim(commandLine.getOptionValue('b')); + + defaultMQAdminExt.start(); + defaultMQAdminExt.createUser(addr, username, password, userType); + + System.out.printf("create user to %s success.%n", addr); + return; + + } else if (commandLine.hasOption('c')) { + String clusterName = StringUtils.trim(commandLine.getOptionValue('c')); + + defaultMQAdminExt.start(); + Set brokerAddrSet = + CommandUtil.fetchMasterAndSlaveAddrByClusterName(defaultMQAdminExt, clusterName); + for (String addr : brokerAddrSet) { + defaultMQAdminExt.createUser(addr, username, password, userType); + System.out.printf("create user to %s success.%n", addr); + } + return; + } + + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/auth/DeleteAclSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/DeleteAclSubCommand.java new file mode 100644 index 0000000..a3553e0 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/DeleteAclSubCommand.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.tools.command.auth; + +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionGroup; +import org.apache.commons.cli.Options; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class DeleteAclSubCommand implements SubCommand { + + @Override + public String commandName() { + return "deleteAcl"; + } + + @Override + public String commandDesc() { + return "Delete acl from cluster."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + OptionGroup optionGroup = new OptionGroup(); + + Option opt = new Option("c", "clusterName", true, "delete acl from which cluster"); + optionGroup.addOption(opt); + + opt = new Option("b", "brokerAddr", true, "delete acl from which broker"); + optionGroup.addOption(opt); + + optionGroup.setRequired(true); + options.addOptionGroup(optionGroup); + + opt = new Option("s", "subject", true, "the subject of acl to delete."); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("r", "resources", true, "the resources of acl to delete"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, + RPCHook rpcHook) throws SubCommandException { + + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + String subject = null; + if (commandLine.hasOption("s")) { + subject = StringUtils.trim(commandLine.getOptionValue("s")); + } + String resource = null; + if (commandLine.hasOption('r')) { + resource = StringUtils.trim(commandLine.getOptionValue("r")); + } + + if (commandLine.hasOption('b')) { + String addr = StringUtils.trim(commandLine.getOptionValue('b')); + + defaultMQAdminExt.start(); + defaultMQAdminExt.deleteAcl(addr, subject, resource); + + System.out.printf("delete acl to %s success.%n", addr); + return; + } else if (commandLine.hasOption('c')) { + String clusterName = StringUtils.trim(commandLine.getOptionValue('c')); + + defaultMQAdminExt.start(); + Set brokerAddrSet = + CommandUtil.fetchMasterAndSlaveAddrByClusterName(defaultMQAdminExt, clusterName); + for (String addr : brokerAddrSet) { + defaultMQAdminExt.deleteAcl(addr, subject, resource); + System.out.printf("delete acl to %s success.%n", addr); + } + return; + } + + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/auth/DeleteUserSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/DeleteUserSubCommand.java new file mode 100644 index 0000000..88344a6 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/DeleteUserSubCommand.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.tools.command.auth; + +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionGroup; +import org.apache.commons.cli.Options; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class DeleteUserSubCommand implements SubCommand { + + @Override + public String commandName() { + return "deleteUser"; + } + + @Override + public String commandDesc() { + return "Delete user from cluster."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + OptionGroup optionGroup = new OptionGroup(); + + Option opt = new Option("c", "clusterName", true, "delete acl from which cluster"); + optionGroup.addOption(opt); + + opt = new Option("b", "brokerAddr", true, "delete user from which broker"); + optionGroup.addOption(opt); + + optionGroup.setRequired(true); + options.addOptionGroup(optionGroup); + + opt = new Option("u", "username", true, "the username of user to delete."); + opt.setRequired(true); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, + RPCHook rpcHook) throws SubCommandException { + + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + String username = StringUtils.trim(commandLine.getOptionValue('u')); + + if (commandLine.hasOption('b')) { + String addr = StringUtils.trim(commandLine.getOptionValue('b')); + + defaultMQAdminExt.start(); + defaultMQAdminExt.deleteUser(addr, username); + + System.out.printf("delete user to %s success.%n", addr); + return; + } else if (commandLine.hasOption('c')) { + String clusterName = StringUtils.trim(commandLine.getOptionValue('c')); + + defaultMQAdminExt.start(); + Set brokerAddrSet = + CommandUtil.fetchMasterAndSlaveAddrByClusterName(defaultMQAdminExt, clusterName); + for (String addr : brokerAddrSet) { + defaultMQAdminExt.deleteUser(addr, username); + System.out.printf("delete user to %s success.%n", addr); + } + return; + } + + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/auth/GetAclSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/GetAclSubCommand.java new file mode 100644 index 0000000..1697bfb --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/GetAclSubCommand.java @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tools.command.auth; + +import java.util.List; +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionGroup; +import org.apache.commons.cli.Options; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.AclInfo; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class GetAclSubCommand implements SubCommand { + + private static final String FORMAT = "%-16s %-10s %-22s %-20s %-24s %-10s%n"; + + @Override + public String commandName() { + return "getAcl"; + } + + @Override + public String commandDesc() { + return "Get acl from cluster."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + OptionGroup optionGroup = new OptionGroup(); + + Option opt = new Option("b", "brokerAddr", true, "get acl for which broker"); + optionGroup.addOption(opt); + + opt = new Option("c", "clusterName", true, "get acl for specified cluster"); + optionGroup.addOption(opt); + + optionGroup.setRequired(true); + options.addOptionGroup(optionGroup); + + opt = new Option("s", "subject", true, "the subject of acl to get"); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, + RPCHook rpcHook) throws SubCommandException { + + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + String subject = StringUtils.trim(commandLine.getOptionValue('s')); + + if (commandLine.hasOption('b')) { + String addr = StringUtils.trim(commandLine.getOptionValue('b')); + defaultMQAdminExt.start(); + + AclInfo aclInfo = defaultMQAdminExt.getAcl(addr, subject); + if (aclInfo != null) { + printAcl(aclInfo); + } + return; + } else if (commandLine.hasOption('c')) { + String clusterName = StringUtils.trim(commandLine.getOptionValue('c')); + + defaultMQAdminExt.start(); + + Set masterSet = + CommandUtil.fetchMasterAddrByClusterName(defaultMQAdminExt, clusterName); + if (CollectionUtils.isEmpty(masterSet)) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed, there is no broker in cluster."); + } + for (String masterAddr : masterSet) { + AclInfo aclInfo = defaultMQAdminExt.getAcl(masterAddr, subject); + if (aclInfo != null) { + printAcl(aclInfo); + } + } + return; + } + + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } + + private void printAcl(AclInfo acl) { + if (acl == null) { + return; + } + System.out.printf(FORMAT, "#Subject", "#PolicyType", "#Resource", "#Actions", "#SourceIp", "#Decision"); + List policyInfos = acl.getPolicies(); + if (CollectionUtils.isEmpty(policyInfos)) { + System.out.printf(FORMAT, acl.getSubject(), "", "", "", "", ""); + } + policyInfos.forEach(policy -> { + List entries = policy.getEntries(); + if (CollectionUtils.isEmpty(entries)) { + return; + } + entries.forEach(entry -> { + System.out.printf(FORMAT, acl.getSubject(), policy.getPolicyType(), entry.getResource(), + entry.getActions(), entry.getSourceIps(), entry.getDecision()); + }); + }); + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/auth/GetUserSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/GetUserSubCommand.java new file mode 100644 index 0000000..061155d --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/GetUserSubCommand.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.tools.command.auth; + +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionGroup; +import org.apache.commons.cli.Options; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.UserInfo; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class GetUserSubCommand implements SubCommand { + + private static final String FORMAT = "%-16s %-22s %-22s %-22s%n"; + + @Override + public String commandName() { + return "getUser"; + } + + @Override + public String commandDesc() { + return "Get user from cluster."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + OptionGroup optionGroup = new OptionGroup(); + + Option opt = new Option("b", "brokerAddr", true, "get user for which broker"); + optionGroup.addOption(opt); + + opt = new Option("c", "clusterName", true, "get user for specified cluster"); + optionGroup.addOption(opt); + + optionGroup.setRequired(true); + options.addOptionGroup(optionGroup); + + opt = new Option("u", "username", true, "the username of user to get"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, + RPCHook rpcHook) throws SubCommandException { + + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + String username = StringUtils.trim(commandLine.getOptionValue('u')); + + if (commandLine.hasOption('b')) { + String addr = StringUtils.trim(commandLine.getOptionValue('b')); + defaultMQAdminExt.start(); + + UserInfo userInfo = defaultMQAdminExt.getUser(addr, username); + if (userInfo != null) { + printUser(userInfo); + } + return; + } else if (commandLine.hasOption('c')) { + String clusterName = StringUtils.trim(commandLine.getOptionValue('c')); + + defaultMQAdminExt.start(); + + Set masterSet = + CommandUtil.fetchMasterAddrByClusterName(defaultMQAdminExt, clusterName); + if (CollectionUtils.isEmpty(masterSet)) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed, there is no broker in cluster."); + } + for (String masterAddr : masterSet) { + UserInfo userInfo = defaultMQAdminExt.getUser(masterAddr, username); + if (userInfo != null) { + printUser(userInfo); + break; + } + } + return; + } + + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } + + private void printUser(UserInfo user) { + if (user == null) { + return; + } + System.out.printf(FORMAT, "#UserName", "#Password", "#UserType", "#UserStatus"); + System.out.printf(FORMAT, user.getUsername(), user.getPassword(), user.getUserType(), user.getUserStatus()); + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/auth/ListAclSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/ListAclSubCommand.java new file mode 100644 index 0000000..cfd7322 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/ListAclSubCommand.java @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tools.command.auth; + +import java.util.List; +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionGroup; +import org.apache.commons.cli.Options; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.AclInfo; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class ListAclSubCommand implements SubCommand { + + private static final String FORMAT = "%-16s %-10s %-22s %-20s %-24s %-10s%n"; + + @Override + public String commandName() { + return "listAcl"; + } + + @Override + public String commandDesc() { + return "List acl from cluster."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + OptionGroup optionGroup = new OptionGroup(); + + Option opt = new Option("b", "brokerAddr", true, "list acl for which broker."); + optionGroup.addOption(opt); + + opt = new Option("c", "clusterName", true, "list acl for specified cluster."); + optionGroup.addOption(opt); + + optionGroup.setRequired(true); + options.addOptionGroup(optionGroup); + + opt = new Option("s", "subject", true, "the subject of acl to filter."); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("r", "resource", true, "the resource of acl to filter."); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, + RPCHook rpcHook) throws SubCommandException { + + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + String subjectFilter = StringUtils.trim(commandLine.getOptionValue('s')); + String resourceFilter = StringUtils.trim(commandLine.getOptionValue('r')); + + if (commandLine.hasOption('b')) { + String addr = StringUtils.trim(commandLine.getOptionValue('b')); + defaultMQAdminExt.start(); + + List aclInfos = defaultMQAdminExt.listAcl(addr, subjectFilter, resourceFilter); + if (CollectionUtils.isNotEmpty(aclInfos)) { + printAcl(aclInfos); + } + return; + } else if (commandLine.hasOption('c')) { + String clusterName = StringUtils.trim(commandLine.getOptionValue('c')); + + defaultMQAdminExt.start(); + + Set masterSet = + CommandUtil.fetchMasterAddrByClusterName(defaultMQAdminExt, clusterName); + if (CollectionUtils.isEmpty(masterSet)) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed, there is no broker in cluster."); + } + for (String masterAddr : masterSet) { + List aclInfos = defaultMQAdminExt.listAcl(masterAddr, subjectFilter, resourceFilter); + if (CollectionUtils.isNotEmpty(aclInfos)) { + printAcl(aclInfos); + } + } + return; + } + + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } + + private void printAcl(List acls) { + System.out.printf(FORMAT, "#Subject", "#PolicyType", "#Resource", "#Actions", "#SourceIp", "#Decision"); + acls.forEach(acl -> { + List policyInfos = acl.getPolicies(); + if (CollectionUtils.isEmpty(policyInfos)) { + System.out.printf(FORMAT, acl.getSubject(), "", "", "", "", ""); + } + policyInfos.forEach(policy -> { + List entries = policy.getEntries(); + if (CollectionUtils.isEmpty(entries)) { + return; + } + entries.forEach(entry -> System.out.printf(FORMAT, acl.getSubject(), policy.getPolicyType(), entry.getResource(), + entry.getActions(), entry.getSourceIps(), entry.getDecision())); + }); + }); + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/auth/ListUserSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/ListUserSubCommand.java new file mode 100644 index 0000000..24eb624 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/ListUserSubCommand.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.tools.command.auth; + +import java.util.List; +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionGroup; +import org.apache.commons.cli.Options; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.UserInfo; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class ListUserSubCommand implements SubCommand { + + private static final String FORMAT = "%-16s %-22s %-22s %-22s%n"; + + @Override + public String commandName() { + return "listUser"; + } + + @Override + public String commandDesc() { + return "List user from cluster."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + OptionGroup optionGroup = new OptionGroup(); + + Option opt = new Option("b", "brokerAddr", true, "list user for which broker"); + optionGroup.addOption(opt); + + opt = new Option("c", "clusterName", true, "list user for specified cluster"); + optionGroup.addOption(opt); + + optionGroup.setRequired(true); + options.addOptionGroup(optionGroup); + + opt = new Option("f", "filter", true, "the filter to list users"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, + RPCHook rpcHook) throws SubCommandException { + + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + String filter = StringUtils.trim(commandLine.getOptionValue('f')); + + if (commandLine.hasOption('b')) { + String addr = StringUtils.trim(commandLine.getOptionValue('b')); + defaultMQAdminExt.start(); + + List userInfos = defaultMQAdminExt.listUser(addr, filter); + if (CollectionUtils.isNotEmpty(userInfos)) { + printUsers(userInfos); + } + return; + } else if (commandLine.hasOption('c')) { + String clusterName = StringUtils.trim(commandLine.getOptionValue('c')); + + defaultMQAdminExt.start(); + + Set masterSet = + CommandUtil.fetchMasterAddrByClusterName(defaultMQAdminExt, clusterName); + if (CollectionUtils.isEmpty(masterSet)) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed, there is no broker in cluster."); + } + for (String masterAddr : masterSet) { + List userInfos = defaultMQAdminExt.listUser(masterAddr, filter); + if (CollectionUtils.isNotEmpty(userInfos)) { + printUsers(userInfos); + System.out.printf("get user from %s success.%n", masterAddr); + break; + } + } + return; + } + + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } + + private void printUsers(List users) { + System.out.printf(FORMAT, "#UserName", "#Password", "#UserType", "#UserStatus"); + users.forEach(user -> System.out.printf(FORMAT, user.getUsername(), user.getPassword(), user.getUserType(), user.getUserStatus())); + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/auth/UpdateAclSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/UpdateAclSubCommand.java new file mode 100644 index 0000000..bccef4f --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/UpdateAclSubCommand.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.tools.command.auth; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionGroup; +import org.apache.commons.cli.Options; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class UpdateAclSubCommand implements SubCommand { + + @Override + public String commandName() { + return "updateAcl"; + } + + @Override + public String commandDesc() { + return "Update acl to cluster."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + OptionGroup optionGroup = new OptionGroup(); + + Option opt = new Option("c", "clusterName", true, "update acl to which cluster"); + optionGroup.addOption(opt); + + opt = new Option("b", "brokerAddr", true, "update acl to which broker"); + optionGroup.addOption(opt); + + optionGroup.setRequired(true); + options.addOptionGroup(optionGroup); + + opt = new Option("s", "subject", true, "the subject of acl to update."); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("r", "resources", true, "the resources of acl to update"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("a", "actions", true, "the actions of acl to update"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("d", "decision", true, "the decision of acl to update"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("i", "sourceIp", true, "the sourceIps of acl to update"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, + RPCHook rpcHook) throws SubCommandException { + + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + String subject = null; + if (commandLine.hasOption('s')) { + subject = StringUtils.trim(commandLine.getOptionValue('s')); + } + List resources = null; + if (commandLine.hasOption('r')) { + resources = Arrays.stream(StringUtils.split(commandLine.getOptionValue('r'), ',')) + .map(StringUtils::trim).collect(Collectors.toList()); + } + List actions = null; + if (commandLine.hasOption('a')) { + actions = Arrays.stream(StringUtils.split(commandLine.getOptionValue('a'), ',')) + .map(StringUtils::trim).collect(Collectors.toList()); + } + + List sourceIps = null; + if (commandLine.hasOption('i')) { + sourceIps = Arrays.stream(StringUtils.split(commandLine.getOptionValue('i'), ',')) + .map(StringUtils::trim).collect(Collectors.toList()); + } + + String decision = null; + if (commandLine.hasOption('d')) { + decision = StringUtils.trim(commandLine.getOptionValue('d')); + } + + if (commandLine.hasOption('b')) { + String addr = StringUtils.trim(commandLine.getOptionValue('b')); + + defaultMQAdminExt.start(); + defaultMQAdminExt.updateAcl(addr, subject, resources, actions, sourceIps, decision); + + System.out.printf("update acl to %s success.%n", addr); + return; + + } else if (commandLine.hasOption('c')) { + String clusterName = StringUtils.trim(commandLine.getOptionValue('c')); + + defaultMQAdminExt.start(); + Set brokerAddrSet = + CommandUtil.fetchMasterAndSlaveAddrByClusterName(defaultMQAdminExt, clusterName); + for (String addr : brokerAddrSet) { + defaultMQAdminExt.updateAcl(addr, subject, resources, actions, sourceIps, decision); + System.out.printf("update acl to %s success.%n", addr); + } + return; + } + + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/auth/UpdateUserSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/UpdateUserSubCommand.java new file mode 100644 index 0000000..ef8544f --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/UpdateUserSubCommand.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.tools.command.auth; + +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionGroup; +import org.apache.commons.cli.Options; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class UpdateUserSubCommand implements SubCommand { + + @Override + public String commandName() { + return "updateUser"; + } + + @Override + public String commandDesc() { + return "Update user to cluster."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + OptionGroup optionGroup = new OptionGroup(); + + Option opt = new Option("c", "clusterName", true, "update user to which cluster"); + optionGroup.addOption(opt); + + opt = new Option("b", "brokerAddr", true, "update user to which broker"); + optionGroup.addOption(opt); + + optionGroup.setRequired(true); + options.addOptionGroup(optionGroup); + + opt = new Option("u", "username", true, "the username of user to update."); + opt.setRequired(true); + options.addOption(opt); + + optionGroup = new OptionGroup(); + opt = new Option("p", "password", true, "the password of user to update"); + optionGroup.addOption(opt); + + opt = new Option("t", "userType", true, "the userType of user to update"); + optionGroup.addOption(opt); + + opt = new Option("s", "userStatus", true, "the userStatus of user to update"); + optionGroup.addOption(opt); + optionGroup.setRequired(true); + + options.addOptionGroup(optionGroup); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, + RPCHook rpcHook) throws SubCommandException { + + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + String username = StringUtils.trim(commandLine.getOptionValue('u')); + String password = StringUtils.trim(commandLine.getOptionValue('p')); + String userType = StringUtils.trim(commandLine.getOptionValue('t')); + String userStatus = StringUtils.trim(commandLine.getOptionValue('s')); + + if (commandLine.hasOption('b')) { + String addr = commandLine.getOptionValue('b').trim(); + + defaultMQAdminExt.start(); + defaultMQAdminExt.updateUser(addr, username, password, userType, userStatus); + + System.out.printf("update user to %s success.%n", addr); + return; + } else if (commandLine.hasOption('c')) { + String clusterName = commandLine.getOptionValue('c').trim(); + + defaultMQAdminExt.start(); + Set brokerAddrSet = + CommandUtil.fetchMasterAndSlaveAddrByClusterName(defaultMQAdminExt, clusterName); + for (String addr : brokerAddrSet) { + defaultMQAdminExt.updateUser(addr, username, password, userType, userStatus); + System.out.printf("update user to %s success.%n", addr); + } + return; + } + + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/broker/BrokerConsumeStatsSubCommad.java b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/BrokerConsumeStatsSubCommad.java new file mode 100644 index 0000000..7658a21 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/BrokerConsumeStatsSubCommad.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.tools.command.broker; + +import java.util.Collections; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.admin.ConsumeStats; +import org.apache.rocketmq.remoting.protocol.admin.OffsetWrapper; +import org.apache.rocketmq.remoting.protocol.body.ConsumeStatsList; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class BrokerConsumeStatsSubCommad implements SubCommand { + + private DefaultMQAdminExt defaultMQAdminExt; + + private DefaultMQAdminExt createMQAdminExt(RPCHook rpcHook) throws SubCommandException { + if (this.defaultMQAdminExt != null) { + return defaultMQAdminExt; + } else { + defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + try { + defaultMQAdminExt.start(); + } + catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } + return defaultMQAdminExt; + } + } + + @Override + public String commandName() { + return "brokerConsumeStats"; + } + + @Override + public String commandDesc() { + return "Fetch broker consume stats data."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("b", "brokerAddr", true, "Broker address"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("t", "timeoutMillis", true, "request timeout Millis"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("l", "level", true, "threshold of print diff"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("o", "order", true, "order topic"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + try { + defaultMQAdminExt = createMQAdminExt(rpcHook); + + String brokerAddr = commandLine.getOptionValue('b').trim(); + boolean isOrder = false; + long timeoutMillis = 50000; + long diffLevel = 0; + if (commandLine.hasOption('o')) { + isOrder = Boolean.parseBoolean(commandLine.getOptionValue('o').trim()); + } + if (commandLine.hasOption('t')) { + timeoutMillis = Long.parseLong(commandLine.getOptionValue('t').trim()); + } + if (commandLine.hasOption('l')) { + diffLevel = Long.parseLong(commandLine.getOptionValue('l').trim()); + } + + ConsumeStatsList consumeStatsList = defaultMQAdminExt.fetchConsumeStatsInBroker(brokerAddr, isOrder, timeoutMillis); + System.out.printf("%-64s %-64s %-32s %-4s %-20s %-20s %-20s %s%n", + "#Topic", + "#Group", + "#Broker Name", + "#QID", + "#Broker Offset", + "#Consumer Offset", + "#Diff", + "#LastTime"); + for (Map> map : consumeStatsList.getConsumeStatsList()) { + for (Map.Entry> entry : map.entrySet()) { + String group = entry.getKey(); + List consumeStatsArray = entry.getValue(); + for (ConsumeStats consumeStats : consumeStatsArray) { + List mqList = new LinkedList<>(); + mqList.addAll(consumeStats.getOffsetTable().keySet()); + Collections.sort(mqList); + for (MessageQueue mq : mqList) { + OffsetWrapper offsetWrapper = consumeStats.getOffsetTable().get(mq); + long diff = offsetWrapper.getBrokerOffset() - offsetWrapper.getConsumerOffset(); + + if (diff < diffLevel) { + continue; + } + String lastTime = "-"; + try { + lastTime = UtilAll.formatDate(new Date(offsetWrapper.getLastTimestamp()), UtilAll.YYYY_MM_DD_HH_MM_SS); + } catch (Exception ignored) { + + } + if (offsetWrapper.getLastTimestamp() > 0) + System.out.printf("%-64s %-64s %-32s %-4d %-20d %-20d %-20d %s%n", + UtilAll.frontStringAtLeast(mq.getTopic(), 64), + group, + UtilAll.frontStringAtLeast(mq.getBrokerName(), 32), + mq.getQueueId(), + offsetWrapper.getBrokerOffset(), + offsetWrapper.getConsumerOffset(), + diff, + lastTime + ); + } + } + } + } + System.out.printf("%nDiff Total: %d%n", consumeStatsList.getTotalDiff()); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/broker/BrokerStatusSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/BrokerStatusSubCommand.java new file mode 100644 index 0000000..ce934f5 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/BrokerStatusSubCommand.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.tools.command.broker; + +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionGroup; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.remoting.RPCHook; +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.protocol.body.KVTable; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class BrokerStatusSubCommand implements SubCommand { + + @Override + public String commandName() { + return "brokerStatus"; + } + + @Override + public String commandDesc() { + return "Fetch broker runtime status data."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + OptionGroup optionGroup = new OptionGroup(); + Option opt = new Option("b", "brokerAddr", true, "Broker address"); + optionGroup.addOption(opt); + + opt = new Option("c", "clusterName", true, "which cluster"); + optionGroup.addOption(opt); + + optionGroup.setRequired(true); + options.addOptionGroup(optionGroup); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + defaultMQAdminExt.start(); + + String brokerAddr = commandLine.hasOption('b') ? commandLine.getOptionValue('b').trim() : null; + String clusterName = commandLine.hasOption('c') ? commandLine.getOptionValue('c').trim() : null; + if (brokerAddr != null) { + printBrokerRuntimeStats(defaultMQAdminExt, brokerAddr, false); + } else if (clusterName != null) { + Set masterSet = + CommandUtil.fetchMasterAndSlaveAddrByClusterName(defaultMQAdminExt, clusterName); + for (String ba : masterSet) { + try { + printBrokerRuntimeStats(defaultMQAdminExt, ba, true); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } + + public void printBrokerRuntimeStats(final DefaultMQAdminExt defaultMQAdminExt, final String brokerAddr, + final boolean printBroker) throws InterruptedException, MQBrokerException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException { + KVTable kvTable = defaultMQAdminExt.fetchBrokerRuntimeStats(brokerAddr); + + TreeMap tmp = new TreeMap<>(); + tmp.putAll(kvTable.getTable()); + + Iterator> it = tmp.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + if (printBroker) { + System.out.printf("%-24s %-32s: %s%n", brokerAddr, next.getKey(), next.getValue()); + } else { + System.out.printf("%-32s: %s%n", next.getKey(), next.getValue()); + } + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/broker/CleanExpiredCQSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/CleanExpiredCQSubCommand.java new file mode 100644 index 0000000..94a49d4 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/CleanExpiredCQSubCommand.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.tools.command.broker; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class CleanExpiredCQSubCommand implements SubCommand { + + @Override + public String commandName() { + return "cleanExpiredCQ"; + } + + @Override + public String commandDesc() { + return "Clean expired ConsumeQueue on broker."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("b", "brokerAddr", true, "Broker address"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "cluster", true, "clustername"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + boolean result = false; + defaultMQAdminExt.start(); + if (commandLine.hasOption('b')) { + String addr = commandLine.getOptionValue('b').trim(); + result = defaultMQAdminExt.cleanExpiredConsumerQueueByAddr(addr); + + } else { + String cluster = commandLine.getOptionValue('c'); + if (null != cluster) + cluster = cluster.trim(); + result = defaultMQAdminExt.cleanExpiredConsumerQueue(cluster); + } + System.out.printf(result ? "success" : "false"); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/broker/CleanUnusedTopicCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/CleanUnusedTopicCommand.java new file mode 100644 index 0000000..045ba8e --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/CleanUnusedTopicCommand.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.tools.command.broker; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class CleanUnusedTopicCommand implements SubCommand { + + @Override + public String commandName() { + return "cleanUnusedTopic"; + } + + @Override + public String commandDesc() { + return "Clean unused topic on broker."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("b", "brokerAddr", true, "Broker address"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "cluster", true, "cluster name"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + boolean result = false; + defaultMQAdminExt.start(); + if (commandLine.hasOption('b')) { + String addr = commandLine.getOptionValue('b').trim(); + result = defaultMQAdminExt.cleanUnusedTopicByAddr(addr); + + } else { + String cluster = commandLine.getOptionValue('c'); + if (null != cluster) + cluster = cluster.trim(); + result = defaultMQAdminExt.cleanUnusedTopic(cluster); + } + System.out.printf(result ? "success" : "false"); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/broker/CommitLogSetReadAheadSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/CommitLogSetReadAheadSubCommand.java new file mode 100644 index 0000000..4fdabfd --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/CommitLogSetReadAheadSubCommand.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.tools.command.broker; + + +import java.io.UnsupportedEncodingException; +import java.util.List; +import java.util.Map; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.remoting.RPCHook; +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.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.admin.MQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class CommitLogSetReadAheadSubCommand implements SubCommand { + private static final String MADV_RANDOM = "1"; + private static final String MADV_NORMAL = "0"; + @Override + public String commandName() { + return "setCommitLogReadAheadMode"; + } + + @Override + public String commandDesc() { + return "Set read ahead mode for all commitlog files."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("b", "brokerAddr", true, "set which broker"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "clusterName", true, "set which cluster"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("m", "commitLogReadAheadMode", true, "set the CommitLog read ahead mode; 0 is default, 1 random read"); + opt.setRequired(true); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) + throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + String mode = commandLine.getOptionValue('m').trim(); + if (!mode.equals(MADV_RANDOM) && !mode.equals(MADV_NORMAL)) { + System.out.printf("set the read mode error; 0 is default, 1 random read\n"); + return; + } + if (commandLine.hasOption('b')) { + String brokerAddr = commandLine.getOptionValue('b').trim(); + defaultMQAdminExt.start(); + setAndPrint(defaultMQAdminExt, String.format("============%s============\n", brokerAddr), brokerAddr, mode); + } else if (commandLine.hasOption('c')) { + String clusterName = commandLine.getOptionValue('c').trim(); + defaultMQAdminExt.start(); + Map> masterAndSlaveMap = CommandUtil.fetchMasterAndSlaveDistinguish(defaultMQAdminExt, clusterName); + for (String masterAddr : masterAndSlaveMap.keySet()) { + setAndPrint(defaultMQAdminExt, String.format("============Master: %s============\n", masterAddr), masterAddr, mode); + for (String slaveAddr : masterAndSlaveMap.get(masterAddr)) { + setAndPrint(defaultMQAdminExt, String.format("============My Master: %s=====Slave: %s============\n", masterAddr, slaveAddr), slaveAddr, mode); + } + } + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } + + protected void setAndPrint(final MQAdminExt defaultMQAdminExt, final String printPrefix, final String addr, final String mode) + throws InterruptedException, RemotingConnectException, UnsupportedEncodingException, RemotingTimeoutException, MQBrokerException, RemotingSendRequestException { + System.out.print(" " + printPrefix); + System.out.printf("commitLog set readAhead mode rstStr" + defaultMQAdminExt.setCommitLogReadAheadMode(addr, mode) + "\n"); + } +} \ No newline at end of file diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/broker/DeleteExpiredCommitLogSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/DeleteExpiredCommitLogSubCommand.java new file mode 100644 index 0000000..142bb7b --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/DeleteExpiredCommitLogSubCommand.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.tools.command.broker; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +/** + * MQAdmin command which deletes expired CommitLog files + */ +public class DeleteExpiredCommitLogSubCommand implements SubCommand { + + @Override + public String commandName() { + return "deleteExpiredCommitLog"; + } + + @Override + public String commandDesc() { + return "Delete expired CommitLog files."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("n", "namesrvAddr", true, "Name server address"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("b", "brokerAddr", true, "Broker address"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "cluster", true, "clustername"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + if (commandLine.hasOption('n')) { + defaultMQAdminExt.setNamesrvAddr(commandLine.getOptionValue('n').trim()); + } + + try { + boolean result = false; + defaultMQAdminExt.start(); + if (commandLine.hasOption('b')) { + String addr = commandLine.getOptionValue('b').trim(); + result = defaultMQAdminExt.deleteExpiredCommitLogByAddr(addr); + + } else { + String cluster = commandLine.getOptionValue('c'); + if (null != cluster) + cluster = cluster.trim(); + result = defaultMQAdminExt.deleteExpiredCommitLog(cluster); + } + System.out.printf(result ? "success" : "false"); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command execute failed.", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/broker/GetBrokerConfigCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/GetBrokerConfigCommand.java new file mode 100644 index 0000000..c4762a2 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/GetBrokerConfigCommand.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.tools.command.broker; + +import java.io.UnsupportedEncodingException; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.remoting.RPCHook; +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.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.admin.MQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class GetBrokerConfigCommand implements SubCommand { + @Override + public String commandName() { + return "getBrokerConfig"; + } + + @Override + public String commandDesc() { + return "Get broker config by cluster or special broker."; + } + + @Override + public Options buildCommandlineOptions(final Options options) { + Option opt = new Option("b", "brokerAddr", true, "get which broker"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "clusterName", true, "get which cluster"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(final CommandLine commandLine, final Options options, + final RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + + if (commandLine.hasOption('b')) { + String brokerAddr = commandLine.getOptionValue('b').trim(); + defaultMQAdminExt.start(); + + getAndPrint(defaultMQAdminExt, + String.format("============%s============\n", brokerAddr), + brokerAddr); + + } else if (commandLine.hasOption('c')) { + String clusterName = commandLine.getOptionValue('c').trim(); + defaultMQAdminExt.start(); + + Map> masterAndSlaveMap + = CommandUtil.fetchMasterAndSlaveDistinguish(defaultMQAdminExt, clusterName); + + for (String masterAddr : masterAndSlaveMap.keySet()) { + + if (masterAddr == null) { + continue; + } + + getAndPrint( + defaultMQAdminExt, + String.format("============Master: %s============\n", masterAddr), + masterAddr + ); + + for (String slaveAddr : masterAndSlaveMap.get(masterAddr)) { + + if (slaveAddr == null) { + continue; + } + + getAndPrint( + defaultMQAdminExt, + String.format("============My Master: %s=====Slave: %s============\n", masterAddr, slaveAddr), + slaveAddr + ); + } + } + } + + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } + + protected void getAndPrint(final MQAdminExt defaultMQAdminExt, final String printPrefix, final String addr) + throws InterruptedException, RemotingConnectException, + UnsupportedEncodingException, RemotingTimeoutException, + MQBrokerException, RemotingSendRequestException { + + System.out.print(printPrefix); + + if (addr.equals(CommandUtil.NO_MASTER_PLACEHOLDER)) { + return; + } + + Properties properties = defaultMQAdminExt.getBrokerConfig(addr); + if (properties == null) { + System.out.printf("Broker[%s] has no config property!\n", addr); + return; + } + + for (Entry entry : properties.entrySet()) { + System.out.printf("%-50s= %s\n", entry.getKey(), entry.getValue()); + } + + System.out.printf("%n"); + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/broker/GetBrokerEpochSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/GetBrokerEpochSubCommand.java new file mode 100644 index 0000000..1a8961e --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/GetBrokerEpochSubCommand.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.tools.command.broker; + +import java.util.List; +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.EpochEntry; +import org.apache.rocketmq.remoting.protocol.body.EpochEntryCache; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class GetBrokerEpochSubCommand implements SubCommand { + @Override + public String commandName() { + return "getBrokerEpoch"; + } + + @Override + public String commandDesc() { + return "Fetch broker epoch entries."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("c", "clusterName", true, "which cluster"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("b", "brokerName", true, "which broker to fetch"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("i", "interval", true, "the interval(second) of get info"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, + RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + if (commandLine.hasOption('i')) { + String interval = commandLine.getOptionValue('i'); + int flushSecond = 3; + if (interval != null && !interval.trim().equals("")) { + flushSecond = Integer.parseInt(interval); + } + + defaultMQAdminExt.start(); + + while (true) { + this.innerExec(commandLine, options, defaultMQAdminExt); + Thread.sleep(flushSecond * 1000); + } + } else { + defaultMQAdminExt.start(); + + this.innerExec(commandLine, options, defaultMQAdminExt); + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } + + private void innerExec(CommandLine commandLine, Options options, + DefaultMQAdminExt defaultMQAdminExt) throws Exception { + if (commandLine.hasOption('b')) { + String brokerName = commandLine.getOptionValue('b').trim(); + final Set brokers = CommandUtil.fetchMasterAndSlaveAddrByBrokerName(defaultMQAdminExt, brokerName); + printData(brokers, defaultMQAdminExt); + } else if (commandLine.hasOption('c')) { + String clusterName = commandLine.getOptionValue('c').trim(); + Set brokers = CommandUtil.fetchMasterAndSlaveAddrByClusterName(defaultMQAdminExt, clusterName); + printData(brokers, defaultMQAdminExt); + } else { + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } + } + + private void printData(Set brokers, DefaultMQAdminExt defaultMQAdminExt) throws Exception { + for (String brokerAddr : brokers) { + final EpochEntryCache epochCache = defaultMQAdminExt.getBrokerEpochCache(brokerAddr); + System.out.printf("\n#clusterName\t%s\n#brokerName\t%s\n#brokerAddr\t%s\n#brokerId\t%d", + epochCache.getClusterName(), epochCache.getBrokerName(), brokerAddr, epochCache.getBrokerId()); + final List epochList = epochCache.getEpochList(); + for (int i = 0; i < epochList.size(); i++) { + final EpochEntry epochEntry = epochList.get(i); + if (i == epochList.size() - 1) { + epochEntry.setEndOffset(epochCache.getMaxOffset()); + } + System.out.printf("\n#Epoch: %s", epochEntry.toString()); + } + System.out.print("\n"); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/broker/GetColdDataFlowCtrInfoSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/GetColdDataFlowCtrInfoSubCommand.java new file mode 100644 index 0000000..34b3ba7 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/GetColdDataFlowCtrInfoSubCommand.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.tools.command.broker; + +import java.io.UnsupportedEncodingException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Map; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.remoting.RPCHook; +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.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.admin.MQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class GetColdDataFlowCtrInfoSubCommand implements SubCommand { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + @Override + public String commandName() { + return "getColdDataFlowCtrInfo"; + } + + @Override + public String commandDesc() { + return "Get cold data flow ctr info."; + } + + @Override + public Options buildCommandlineOptions(final Options options) { + Option opt = new Option("b", "brokerAddr", true, "get from which broker"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "clusterName", true, "get from which cluster"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(final CommandLine commandLine, final Options options, final RPCHook rpcHook) + throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + if (commandLine.hasOption('b')) { + String brokerAddr = commandLine.getOptionValue('b').trim(); + defaultMQAdminExt.start(); + getAndPrint(defaultMQAdminExt, String.format("============%s============\n", brokerAddr), brokerAddr); + } else if (commandLine.hasOption('c')) { + String clusterName = commandLine.getOptionValue('c').trim(); + defaultMQAdminExt.start(); + Map> masterAndSlaveMap = CommandUtil.fetchMasterAndSlaveDistinguish(defaultMQAdminExt, clusterName); + for (String masterAddr : masterAndSlaveMap.keySet()) { + getAndPrint(defaultMQAdminExt, String.format("============Master: %s============\n", masterAddr), masterAddr); + for (String slaveAddr : masterAndSlaveMap.get(masterAddr)) { + getAndPrint(defaultMQAdminExt, String.format("============My Master: %s=====Slave: %s============\n", masterAddr, slaveAddr), slaveAddr); + } + } + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } + + protected void getAndPrint(final MQAdminExt defaultMQAdminExt, final String printPrefix, final String addr) + throws InterruptedException, RemotingConnectException, + UnsupportedEncodingException, RemotingTimeoutException, + MQBrokerException, RemotingSendRequestException { + + System.out.print(" " + printPrefix); + String rstStr = defaultMQAdminExt.getColdDataFlowCtrInfo(addr); + if (rstStr == null) { + System.out.printf("Broker[%s] has no cold ctr table !\n", addr); + return; + } + JSONObject jsonObject = JSON.parseObject(rstStr); + Map runtimeTable = (Map)jsonObject.get("runtimeTable"); + runtimeTable.entrySet().stream().forEach(i -> { + JSONObject value = i.getValue(); + Date lastColdReadTimeMillsDate = new Date(Long.parseLong(String.valueOf(value.get("lastColdReadTimeMills")))); + value.put("lastColdReadTimeFormat", sdf.format(lastColdReadTimeMillsDate)); + value.remove("lastColdReadTimeMills"); + + Date createTimeMillsDate = new Date(Long.parseLong(String.valueOf(value.get("createTimeMills")))); + value.put("createTimeFormat", sdf.format(createTimeMillsDate)); + value.remove("createTimeMills"); + }); + + String formatStr = JSON.toJSONString(jsonObject, true); + System.out.printf(formatStr); + System.out.printf("%n"); + } + +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/broker/RemoveColdDataFlowCtrGroupConfigSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/RemoveColdDataFlowCtrGroupConfigSubCommand.java new file mode 100644 index 0000000..f204074 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/RemoveColdDataFlowCtrGroupConfigSubCommand.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.tools.command.broker; + +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class RemoveColdDataFlowCtrGroupConfigSubCommand implements SubCommand { + + @Override + public String commandName() { + return "removeColdDataFlowCtrGroupConfig"; + } + + @Override + public String commandDesc() { + return "Remove consumer from cold ctr config."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("b", "brokerAddr", true, "update which broker"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "clusterName", true, "update which cluster"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("g", "consumerGroup", true, "the consumer group will remove from the config"); + opt.setRequired(true); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + String consumerGroup = commandLine.getOptionValue('g').trim(); + if (commandLine.hasOption('b')) { + String brokerAddr = commandLine.getOptionValue('b').trim(); + defaultMQAdminExt.start(); + defaultMQAdminExt.removeColdDataFlowCtrGroupConfig(brokerAddr, consumerGroup); + System.out.printf("remove broker cold read threshold success, %s\n", brokerAddr); + return; + } else if (commandLine.hasOption('c')) { + String clusterName = commandLine.getOptionValue('c').trim(); + defaultMQAdminExt.start(); + Set masterSet = CommandUtil.fetchMasterAddrByClusterName(defaultMQAdminExt, clusterName); + for (String brokerAddr : masterSet) { + try { + defaultMQAdminExt.removeColdDataFlowCtrGroupConfig(brokerAddr, consumerGroup); + System.out.printf("remove broker cold read threshold success, %s\n", brokerAddr); + } catch (Exception e) { + e.printStackTrace(); + } + } + return; + } + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/broker/ResetMasterFlushOffsetSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/ResetMasterFlushOffsetSubCommand.java new file mode 100644 index 0000000..90451b5 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/ResetMasterFlushOffsetSubCommand.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.tools.command.broker; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class ResetMasterFlushOffsetSubCommand implements SubCommand { + @Override + public String commandName() { + return "resetMasterFlushOffset"; + } + + @Override + public String commandDesc() { + return "Reset master flush offset in slave."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("b", "brokerAddr", true, "which broker to reset"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("o", "offset", true, "the offset to reset at"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, + RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + defaultMQAdminExt.start(); + String brokerAddr = commandLine.getOptionValue('b').trim(); + long masterFlushOffset = Long.parseLong(commandLine.getOptionValue('o').trim()); + + defaultMQAdminExt.resetMasterFlushOffset(brokerAddr, masterFlushOffset); + System.out.printf("reset master flush offset to %d success%n", masterFlushOffset); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/broker/SendMsgStatusCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/SendMsgStatusCommand.java new file mode 100644 index 0000000..37561c4 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/SendMsgStatusCommand.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.tools.command.broker; + +import java.io.UnsupportedEncodingException; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class SendMsgStatusCommand implements SubCommand { + + private static Message buildMessage(final String topic, final int messageSize) throws UnsupportedEncodingException { + Message msg = new Message(); + msg.setTopic(topic); + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < messageSize; i += 11) { + sb.append("hello jodie"); + } + msg.setBody(sb.toString().getBytes(MixAll.DEFAULT_CHARSET)); + return msg; + } + + @Override + public String commandName() { + return "sendMsgStatus"; + } + + @Override + public String commandDesc() { + return "Send msg to broker."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("b", "brokerName", true, "Broker Name e.g. clusterName_brokerName as DefaultCluster_broker-a"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("s", "messageSize", true, "Message Size, Default: 128"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "count", true, "send message count, Default: 50"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + final DefaultMQProducer producer = new DefaultMQProducer("PID_SMSC", rpcHook); + producer.setInstanceName("PID_SMSC_" + System.currentTimeMillis()); + + try { + producer.start(); + String brokerName = commandLine.getOptionValue('b').trim(); + int messageSize = commandLine.hasOption('s') ? Integer.parseInt(commandLine.getOptionValue('s')) : 128; + int count = commandLine.hasOption('c') ? Integer.parseInt(commandLine.getOptionValue('c')) : 50; + + producer.send(buildMessage(brokerName, 16)); + + for (int i = 0; i < count; i++) { + long begin = System.currentTimeMillis(); + SendResult result = producer.send(buildMessage(brokerName, messageSize)); + System.out.printf("rt=%sms, SendResult=%s%n", System.currentTimeMillis() - begin, result); + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + producer.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/broker/UpdateBrokerConfigSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/UpdateBrokerConfigSubCommand.java new file mode 100644 index 0000000..62816ef --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/UpdateBrokerConfigSubCommand.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.tools.command.broker; + +import java.util.Properties; +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class UpdateBrokerConfigSubCommand implements SubCommand { + + @Override + public String commandName() { + return "updateBrokerConfig"; + } + + @Override + public String commandDesc() { + return "Update broker's config."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("b", "brokerAddr", true, "update which broker"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "clusterName", true, "update which cluster"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("a", "updateAllBroker", true, "update all brokers include slave"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("k", "key", true, "config key"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("v", "value", true, "config value"); + opt.setRequired(true); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + String key = commandLine.getOptionValue('k').trim(); + String value = commandLine.getOptionValue('v').trim(); + Properties properties = new Properties(); + properties.put(key, value); + + if (commandLine.hasOption('b')) { + String brokerAddr = commandLine.getOptionValue('b').trim(); + + defaultMQAdminExt.start(); + + defaultMQAdminExt.updateBrokerConfig(brokerAddr, properties); + System.out.printf("update broker config success, %s\n", brokerAddr); + return; + + } else if (commandLine.hasOption('c')) { + String clusterName = commandLine.getOptionValue('c').trim(); + + defaultMQAdminExt.start(); + + Set brokerAddrSet; + + if (commandLine.hasOption('a')) { + brokerAddrSet = CommandUtil.fetchMasterAndSlaveAddrByClusterName(defaultMQAdminExt, clusterName); + } else { + brokerAddrSet = CommandUtil.fetchMasterAddrByClusterName(defaultMQAdminExt, clusterName); + } + + for (String brokerAddr : brokerAddrSet) { + try { + defaultMQAdminExt.updateBrokerConfig(brokerAddr, properties); + System.out.printf("update broker config success, %s\n", brokerAddr); + } catch (Exception e) { + e.printStackTrace(); + } + } + return; + } + + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/broker/UpdateColdDataFlowCtrGroupConfigSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/UpdateColdDataFlowCtrGroupConfigSubCommand.java new file mode 100644 index 0000000..8d1a000 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/broker/UpdateColdDataFlowCtrGroupConfigSubCommand.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.tools.command.broker; + + +import java.util.Properties; +import java.util.Set; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class UpdateColdDataFlowCtrGroupConfigSubCommand implements SubCommand { + + @Override + public String commandName() { + return "updateColdDataFlowCtrGroupConfig"; + } + + @Override + public String commandDesc() { + return "Add or update cold data flow ctr group config."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("b", "brokerAddr", true, "update which broker"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "clusterName", true, "update which cluster"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("g", "consumerGroup", true, "specific consumerGroup"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("v", "threshold", true, "cold read threshold value"); + opt.setRequired(true); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + try { + String key = commandLine.getOptionValue('g').trim(); + String value = commandLine.getOptionValue('v').trim(); + Properties properties = new Properties(); + properties.put(key, value); + + if (commandLine.hasOption('b')) { + String brokerAddr = commandLine.getOptionValue('b').trim(); + defaultMQAdminExt.start(); + defaultMQAdminExt.updateColdDataFlowCtrGroupConfig(brokerAddr, properties); + System.out.printf("updateColdDataFlowCtrGroupConfig success, %s\n", brokerAddr); + return; + } else if (commandLine.hasOption('c')) { + String clusterName = commandLine.getOptionValue('c').trim(); + defaultMQAdminExt.start(); + Set masterSet = CommandUtil.fetchMasterAddrByClusterName(defaultMQAdminExt, clusterName); + for (String brokerAddr : masterSet) { + try { + defaultMQAdminExt.updateColdDataFlowCtrGroupConfig(brokerAddr, properties); + System.out.printf("updateColdDataFlowCtrGroupConfig success, %s\n", brokerAddr); + } catch (Exception e) { + e.printStackTrace(); + } + } + return; + } + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} \ No newline at end of file diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/cluster/CLusterSendMsgRTCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/cluster/CLusterSendMsgRTCommand.java new file mode 100644 index 0000000..d755e9e --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/cluster/CLusterSendMsgRTCommand.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.tools.command.cluster; + +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; +import java.util.TreeSet; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class CLusterSendMsgRTCommand implements SubCommand { + + public static void main(String[] args) { + } + + @Override + public String commandName() { + return "clusterRT"; + } + + @Override + public String commandDesc() { + return "List All clusters Message Send RT."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("a", "amount", true, "message amount | default 100"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("s", "size", true, "message size | default 128 Byte"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "cluster", true, "cluster name | default display all cluster"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("p", "print log", true, "print as tlog | default false"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("m", "machine room", true, "machine room name | default noname"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("i", "interval", true, "print interval | default 10 seconds"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + DefaultMQProducer producer = new DefaultMQProducer(rpcHook); + producer.setProducerGroup(Long.toString(System.currentTimeMillis())); + + try { + defaultMQAdminExt.start(); + producer.start(); + + ClusterInfo clusterInfoSerializeWrapper = defaultMQAdminExt.examineBrokerClusterInfo(); + Map> clusterAddr = clusterInfoSerializeWrapper + .getClusterAddrTable(); + + Set clusterNames = null; + + long amount = !commandLine.hasOption('a') ? 100 : Long.parseLong(commandLine + .getOptionValue('a').trim()); + + long size = !commandLine.hasOption('s') ? 128 : Long.parseLong(commandLine + .getOptionValue('s').trim()); + + long interval = !commandLine.hasOption('i') ? 10 : Long.parseLong(commandLine + .getOptionValue('i').trim()); + + boolean printAsTlog = commandLine.hasOption('p') && Boolean.parseBoolean(commandLine.getOptionValue('p').trim()); + + String machineRoom = !commandLine.hasOption('m') ? "noname" : commandLine + .getOptionValue('m').trim(); + + if (commandLine.hasOption('c')) { + clusterNames = new TreeSet<>(); + clusterNames.add(commandLine.getOptionValue('c').trim()); + } else { + clusterNames = clusterAddr.keySet(); + } + + if (!printAsTlog) { + System.out.printf("%-24s %-24s %-4s %-8s %-8s%n", + "#Cluster Name", + "#Broker Name", + "#RT", + "#successCount", + "#failCount" + ); + } + + while (true) { + for (String clusterName : clusterNames) { + Set brokerNames = clusterAddr.get(clusterName); + if (brokerNames == null) { + System.out.printf("cluster [%s] not exist", clusterName); + break; + } + + for (String brokerName : brokerNames) { + Message msg = new Message(brokerName, getStringBySize(size).getBytes(MixAll.DEFAULT_CHARSET)); + long start = 0; + long end = 0; + long elapsed = 0; + int successCount = 0; + int failCount = 0; + + for (int i = 0; i < amount; i++) { + start = System.currentTimeMillis(); + try { + producer.send(msg); + successCount++; + end = System.currentTimeMillis(); + } catch (Exception e) { + failCount++; + end = System.currentTimeMillis(); + } + + if (i != 0) { + elapsed += end - start; + } + } + + double rt = (double) elapsed / (amount - 1); + if (!printAsTlog) { + System.out.printf("%-24s %-24s %-8s %-16s %-16s%n", + clusterName, + brokerName, + String.format("%.2f", rt), + successCount, + failCount + ); + } else { + System.out.printf("%s", String.format("%s|%s|%s|%s|%s%n", getCurTime(), + machineRoom, clusterName, brokerName, + new BigDecimal(rt).setScale(0, BigDecimal.ROUND_HALF_UP))); + } + + } + + } + + Thread.sleep(interval * 1000); + } + + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + producer.shutdown(); + } + } + + public String getStringBySize(long size) { + StringBuilder res = new StringBuilder(); + for (int i = 0; i < size; i++) { + res.append('a'); + } + return res.toString(); + } + + public String getCurTime() { + String fromTimeZone = "GMT+8"; + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date date = new Date(); + format.setTimeZone(TimeZone.getTimeZone(fromTimeZone)); + String chinaDate = format.format(date); + return chinaDate; + } + +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/cluster/ClusterListSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/cluster/ClusterListSubCommand.java new file mode 100644 index 0000000..8103f4c --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/cluster/ClusterListSubCommand.java @@ -0,0 +1,304 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tools.command.cluster; + +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.body.KVTable; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class ClusterListSubCommand implements SubCommand { + + @Override + public String commandName() { + return "clusterList"; + } + + @Override + public String commandDesc() { + return "List cluster infos."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("m", "moreStats", false, "Print more stats"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("i", "interval", true, "specify intervals numbers, it is in seconds"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "clusterName", true, "which cluster"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(final CommandLine commandLine, final Options options, + RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + long printInterval = 1; + boolean enableInterval = commandLine.hasOption('i'); + + if (enableInterval) { + printInterval = Long.parseLong(commandLine.getOptionValue('i')) * 1000; + } + + String clusterName = commandLine.hasOption('c') ? commandLine.getOptionValue('c').trim() : ""; + + try { + defaultMQAdminExt.start(); + long i = 0; + + do { + if (i++ > 0) { + Thread.sleep(printInterval); + } + + ClusterInfo clusterInfo = defaultMQAdminExt.examineBrokerClusterInfo(); + + Set clusterNames = getTargetClusterNames(clusterName, clusterInfo); + + if (commandLine.hasOption('m')) { + this.printClusterMoreStats(clusterNames, defaultMQAdminExt, clusterInfo); + } else { + this.printClusterBaseInfo(clusterNames, defaultMQAdminExt, clusterInfo); + } + } + while (enableInterval); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } + + private Set getTargetClusterNames(String clusterName, ClusterInfo clusterInfo) { + if (StringUtils.isEmpty(clusterName)) { + return clusterInfo.getClusterAddrTable().keySet(); + } else { + Set clusterNames = new TreeSet<>(); + clusterNames.add(clusterName); + return clusterNames; + } + } + + private void printClusterMoreStats(final Set clusterNames, + final DefaultMQAdminExt defaultMQAdminExt, + final ClusterInfo clusterInfo) { + System.out.printf("%-16s %-32s %14s %14s %14s %14s%n", + "#Cluster Name", + "#Broker Name", + "#InTotalYest", + "#OutTotalYest", + "#InTotalToday", + "#OutTotalToday" + ); + + for (String clusterName : clusterNames) { + TreeSet brokerNameTreeSet = new TreeSet<>(); + Set brokerNameSet = clusterInfo.getClusterAddrTable().get(clusterName); + if (brokerNameSet != null && !brokerNameSet.isEmpty()) { + brokerNameTreeSet.addAll(brokerNameSet); + } + + for (String brokerName : brokerNameTreeSet) { + BrokerData brokerData = clusterInfo.getBrokerAddrTable().get(brokerName); + if (brokerData != null) { + Iterator> itAddr = brokerData.getBrokerAddrs().entrySet().iterator(); + while (itAddr.hasNext()) { + Map.Entry next1 = itAddr.next(); + long inTotalYest = 0; + long outTotalYest = 0; + long inTotalToday = 0; + long outTotalToday = 0; + + try { + KVTable kvTable = defaultMQAdminExt.fetchBrokerRuntimeStats(next1.getValue()); + String msgPutTotalYesterdayMorning = kvTable.getTable().get("msgPutTotalYesterdayMorning"); + String msgPutTotalTodayMorning = kvTable.getTable().get("msgPutTotalTodayMorning"); + String msgPutTotalTodayNow = kvTable.getTable().get("msgPutTotalTodayNow"); + String msgGetTotalYesterdayMorning = kvTable.getTable().get("msgGetTotalYesterdayMorning"); + String msgGetTotalTodayMorning = kvTable.getTable().get("msgGetTotalTodayMorning"); + String msgGetTotalTodayNow = kvTable.getTable().get("msgGetTotalTodayNow"); + + inTotalYest = Long.parseLong(msgPutTotalTodayMorning) - Long.parseLong(msgPutTotalYesterdayMorning); + outTotalYest = Long.parseLong(msgGetTotalTodayMorning) - Long.parseLong(msgGetTotalYesterdayMorning); + + inTotalToday = Long.parseLong(msgPutTotalTodayNow) - Long.parseLong(msgPutTotalTodayMorning); + outTotalToday = Long.parseLong(msgGetTotalTodayNow) - Long.parseLong(msgGetTotalTodayMorning); + + } catch (Exception ignored) { + } + + System.out.printf("%-16s %-32s %14d %14d %14d %14d%n", + clusterName, + brokerName, + inTotalYest, + outTotalYest, + inTotalToday, + outTotalToday + ); + } + } + } + } + } + + private void printClusterBaseInfo(final Set clusterNames, + final DefaultMQAdminExt defaultMQAdminExt, + final ClusterInfo clusterInfo) { + System.out.printf("%-22s %-22s %-4s %-22s %-16s %16s %30s %-22s %-11s %-12s %-8s %-10s%n", + "#Cluster Name", + "#Broker Name", + "#BID", + "#Addr", + "#Version", + "#InTPS(LOAD)", + "#OutTPS(LOAD)", + "#Timer(Progress)", + "#PCWait(ms)", + "#Hour", + "#SPACE", + "#ACTIVATED" + ); + + for (String clusterName : clusterNames) { + TreeSet brokerNameTreeSet = new TreeSet<>(); + Set brokerNameSet = clusterInfo.getClusterAddrTable().get(clusterName); + if (brokerNameSet != null && !brokerNameSet.isEmpty()) { + brokerNameTreeSet.addAll(brokerNameSet); + } + + for (String brokerName : brokerNameTreeSet) { + BrokerData brokerData = clusterInfo.getBrokerAddrTable().get(brokerName); + if (brokerData != null) { + Iterator> itAddr = brokerData.getBrokerAddrs().entrySet().iterator(); + while (itAddr.hasNext()) { + Map.Entry next1 = itAddr.next(); + double in = 0; + double out = 0; + String version = ""; + String sendThreadPoolQueueSize = ""; + String pullThreadPoolQueueSize = ""; + String ackThreadPoolQueueSize = ""; + String sendThreadPoolQueueHeadWaitTimeMills = ""; + String pullThreadPoolQueueHeadWaitTimeMills = ""; + String ackThreadPoolQueueHeadWaitTimeMills = ""; + String pageCacheLockTimeMills = ""; + String earliestMessageTimeStamp = ""; + String commitLogDiskRatio = ""; + long timerReadBehind = 0; + long timerOffsetBehind = 0; + long timerCongestNum = 0; + float timerEnqueueTps = 0.0f; + float timerDequeueTps = 0.0f; + boolean isBrokerActive = false; + try { + KVTable kvTable = defaultMQAdminExt.fetchBrokerRuntimeStats(next1.getValue()); + isBrokerActive = Boolean.parseBoolean(kvTable.getTable().get("brokerActive")); + String putTps = kvTable.getTable().get("putTps"); + String getTransferredTps = kvTable.getTable().get("getTransferredTps"); + + sendThreadPoolQueueSize = kvTable.getTable().get("sendThreadPoolQueueSize"); + pullThreadPoolQueueSize = kvTable.getTable().get("pullThreadPoolQueueSize"); + ackThreadPoolQueueSize = kvTable.getTable().getOrDefault("ackThreadPoolQueueSize", "N"); + + sendThreadPoolQueueHeadWaitTimeMills = kvTable.getTable().get("sendThreadPoolQueueHeadWaitTimeMills"); + pullThreadPoolQueueHeadWaitTimeMills = kvTable.getTable().get("pullThreadPoolQueueHeadWaitTimeMills"); + ackThreadPoolQueueHeadWaitTimeMills = kvTable.getTable().getOrDefault("ackThreadPoolQueueHeadWaitTimeMills", "N"); + pageCacheLockTimeMills = kvTable.getTable().get("pageCacheLockTimeMills"); + earliestMessageTimeStamp = kvTable.getTable().get("earliestMessageTimeStamp"); + commitLogDiskRatio = kvTable.getTable().get("commitLogDiskRatio"); + + try { + timerReadBehind = Long.parseLong(kvTable.getTable().get("timerReadBehind")); + timerOffsetBehind = Long.parseLong(kvTable.getTable().get("timerOffsetBehind")); + timerCongestNum = Long.parseLong(kvTable.getTable().get("timerCongestNum")); + timerEnqueueTps = Float.parseFloat(kvTable.getTable().get("timerEnqueueTps")); + timerDequeueTps = Float.parseFloat(kvTable.getTable().get("timerDequeueTps")); + } catch (Throwable ignored) { + } + + version = kvTable.getTable().get("brokerVersionDesc"); + + if (StringUtils.isNotBlank(putTps)) { + String[] tpss = putTps.split(" "); + if (tpss.length > 0) { + in = Double.parseDouble(tpss[0]); + } + } + + if (StringUtils.isNotBlank(getTransferredTps)) { + String[] tpss = getTransferredTps.split(" "); + if (tpss.length > 0) { + out = Double.parseDouble(tpss[0]); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + + double hour = 0.0; + double space = 0.0; + + if (earliestMessageTimeStamp != null && earliestMessageTimeStamp.length() > 0) { + long mills = System.currentTimeMillis() - Long.parseLong(earliestMessageTimeStamp); + hour = mills / 1000.0 / 60.0 / 60.0; + } + + if (commitLogDiskRatio != null && commitLogDiskRatio.length() > 0) { + space = Double.parseDouble(commitLogDiskRatio); + } + + System.out.printf("%-22s %-22s %-4s %-22s %-16s %16s %30s %-22s %11s %-12s %-8s %10s%n", + clusterName, + brokerName, + next1.getKey(), + next1.getValue(), + version, + String.format("%9.2f(%s,%sms)", in, sendThreadPoolQueueSize, sendThreadPoolQueueHeadWaitTimeMills), + String.format("%9.2f(%s,%sms|%s,%sms)", out, pullThreadPoolQueueSize, pullThreadPoolQueueHeadWaitTimeMills, ackThreadPoolQueueSize, ackThreadPoolQueueHeadWaitTimeMills), + String.format("%d-%d(%.1fw, %.1f, %.1f)", timerReadBehind, timerOffsetBehind, timerCongestNum / 10000.0f, timerEnqueueTps, timerDequeueTps), + pageCacheLockTimeMills, + String.format("%2.2f", hour), + String.format("%.4f", space), + isBrokerActive + ); + } + } + } + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/connection/ConsumerConnectionSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/connection/ConsumerConnectionSubCommand.java new file mode 100644 index 0000000..35f73d8 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/connection/ConsumerConnectionSubCommand.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.tools.command.connection; + +import java.util.Iterator; +import java.util.Map.Entry; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.Connection; +import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class ConsumerConnectionSubCommand implements SubCommand { + + @Override + public String commandName() { + return "consumerConnection"; + } + + @Override + public String commandDesc() { + return "Query consumer's socket connection, client version and subscription."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("g", "consumerGroup", true, "consumer group name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("b", "brokerAddr", true, "broker address"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + defaultMQAdminExt.start(); + + String group = commandLine.getOptionValue('g').trim(); + + ConsumerConnection cc = commandLine.hasOption('b') + ? defaultMQAdminExt.examineConsumerConnectionInfo(group, commandLine.getOptionValue('b').trim()) + : defaultMQAdminExt.examineConsumerConnectionInfo(group); + + System.out.printf("%-36s %-22s %-10s %s%n", "#ClientId", "#ClientAddr", "#Language", "#Version"); + for (Connection conn : cc.getConnectionSet()) { + System.out.printf("%-36s %-22s %-10s %s%n", + conn.getClientId(), + conn.getClientAddr(), + conn.getLanguage(), + MQVersion.getVersionDesc(conn.getVersion()) + ); + } + + System.out.printf("%nBelow is subscription:\n"); + Iterator> it = cc.getSubscriptionTable().entrySet().iterator(); + System.out.printf("%-20s %s%n", "#Topic", "#SubExpression"); + while (it.hasNext()) { + Entry entry = it.next(); + SubscriptionData sd = entry.getValue(); + System.out.printf("%-20s %s%n", + sd.getTopic(), + sd.getSubString() + ); + } + + System.out.printf("\n"); + System.out.printf("ConsumeType: %s%n", cc.getConsumeType()); + System.out.printf("MessageModel: %s%n", cc.getMessageModel()); + System.out.printf("ConsumeFromWhere: %s%n", cc.getConsumeFromWhere()); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/connection/ProducerConnectionSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/connection/ProducerConnectionSubCommand.java new file mode 100644 index 0000000..bde674a --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/connection/ProducerConnectionSubCommand.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.tools.command.connection; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.Connection; +import org.apache.rocketmq.remoting.protocol.body.ProducerConnection; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class ProducerConnectionSubCommand implements SubCommand { + + @Override + public String commandName() { + return "producerConnection"; + } + + @Override + public String commandDesc() { + return "Query producer's socket connection and client version."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("g", "producerGroup", true, "producer group name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("t", "topic", true, "topic name"); + opt.setRequired(true); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + if (commandLine.hasOption('n')) { + defaultMQAdminExt.setNamesrvAddr(commandLine.getOptionValue('n').trim()); + } + + try { + defaultMQAdminExt.start(); + + String group = commandLine.getOptionValue('g').trim(); + String topic = commandLine.getOptionValue('t').trim(); + + ProducerConnection pc = defaultMQAdminExt.examineProducerConnectionInfo(group, topic); + + int i = 1; + for (Connection conn : pc.getConnectionSet()) { + System.out.printf("%04d %-32s %-22s %-8s %s%n", + i++, + conn.getClientId(), + conn.getClientAddr(), + conn.getLanguage(), + MQVersion.getVersionDesc(conn.getVersion()) + ); + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/ConsumerProgressSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/ConsumerProgressSubCommand.java new file mode 100644 index 0000000..b638dcf --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/ConsumerProgressSubCommand.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.tools.command.consumer; + +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.admin.ConsumeStats; +import org.apache.rocketmq.remoting.protocol.admin.OffsetWrapper; +import org.apache.rocketmq.remoting.protocol.body.Connection; +import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.body.TopicList; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +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 org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class ConsumerProgressSubCommand implements SubCommand { + private static final Logger log = LoggerFactory.getLogger(ConsumerProgressSubCommand.class); + + @Override + public String commandName() { + return "consumerProgress"; + } + + @Override + public String commandDesc() { + return "Query consumer's progress, speed."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("g", "groupName", true, "consumer group name"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("t", "topicName", true, "topic name"); + opt.setRequired(false); + options.addOption(opt); + + Option optionShowClientIP = new Option("s", "showClientIP", true, "Show Client IP per Queue"); + optionShowClientIP.setRequired(false); + options.addOption(optionShowClientIP); + + opt = new Option("c", "cluster", true, "Cluster name or lmq parent topic, lmq is used to find the route."); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + private Map getMessageQueueAllocationResult(DefaultMQAdminExt defaultMQAdminExt, + String groupName) { + Map results = new HashMap<>(); + try { + ConsumerConnection consumerConnection = defaultMQAdminExt.examineConsumerConnectionInfo(groupName); + for (Connection connection : consumerConnection.getConnectionSet()) { + String clientId = connection.getClientId(); + ConsumerRunningInfo consumerRunningInfo = defaultMQAdminExt.getConsumerRunningInfo(groupName, clientId, + false, false); + for (MessageQueue messageQueue : consumerRunningInfo.getMqTable().keySet()) { + results.put(messageQueue, clientId.split("@")[0]); + } + } + } catch (Exception e) { + log.error("getMqAllocationsResult error, ", e); + } + return results; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + if (commandLine.hasOption('n')) { + defaultMQAdminExt.setNamesrvAddr(commandLine.getOptionValue('n').trim()); + } + + try { + defaultMQAdminExt.start(); + + boolean showClientIP = commandLine.hasOption('s') + && "true".equalsIgnoreCase(commandLine.getOptionValue('s')); + + String clusterName = commandLine.hasOption('c') ? commandLine.getOptionValue('c').trim() : null; + + if (commandLine.hasOption('g')) { + String consumerGroup = commandLine.getOptionValue('g').trim(); + String topicName = commandLine.hasOption('t') ? commandLine.getOptionValue('t').trim() : null; + ConsumeStats consumeStats; + if (topicName == null) { + consumeStats = defaultMQAdminExt.examineConsumeStats(consumerGroup); + } else { + consumeStats = defaultMQAdminExt.examineConsumeStats(clusterName, consumerGroup, topicName); + } + List mqList = new LinkedList<>(consumeStats.getOffsetTable().keySet()); + Collections.sort(mqList); + + Map messageQueueAllocationResult = null; + if (showClientIP) { + messageQueueAllocationResult = getMessageQueueAllocationResult(defaultMQAdminExt, consumerGroup); + } + if (showClientIP) { + System.out.printf("%-64s %-32s %-4s %-20s %-20s %-20s %-20s %-20s%s%n", + "#Topic", + "#Broker Name", + "#QID", + "#Broker Offset", + "#Consumer Offset", + "#Client IP", + "#Diff", + "#Inflight", + "#LastTime"); + } else { + System.out.printf("%-64s %-32s %-4s %-20s %-20s %-20s %-20s%s%n", + "#Topic", + "#Broker Name", + "#QID", + "#Broker Offset", + "#Consumer Offset", + "#Diff", + "#Inflight", + "#LastTime"); + } + long diffTotal = 0L; + long inflightTotal = 0L; + for (MessageQueue mq : mqList) { + OffsetWrapper offsetWrapper = consumeStats.getOffsetTable().get(mq); + long diff = offsetWrapper.getBrokerOffset() - offsetWrapper.getConsumerOffset(); + long inflight = offsetWrapper.getPullOffset() - offsetWrapper.getConsumerOffset(); + diffTotal += diff; + inflightTotal += inflight; + String lastTime = ""; + try { + if (offsetWrapper.getLastTimestamp() == 0) { + lastTime = "N/A"; + } else { + lastTime = UtilAll.formatDate(new Date(offsetWrapper.getLastTimestamp()), UtilAll.YYYY_MM_DD_HH_MM_SS); + } + } catch (Exception e) { + // ignore + } + + String clientIP = null; + if (showClientIP) { + clientIP = messageQueueAllocationResult.get(mq); + } + if (showClientIP) { + System.out.printf("%-64s %-32s %-4d %-20d %-20d %-20s %-20d %-20d %s%n", + UtilAll.frontStringAtLeast(mq.getTopic(), 64), + UtilAll.frontStringAtLeast(mq.getBrokerName(), 32), + mq.getQueueId(), + offsetWrapper.getBrokerOffset(), + offsetWrapper.getConsumerOffset(), + null != clientIP ? clientIP : "N/A", + diff, + inflight, + lastTime + ); + } else { + System.out.printf("%-64s %-32s %-4d %-20d %-20d %-20d %-20d %s%n", + UtilAll.frontStringAtLeast(mq.getTopic(), 64), + UtilAll.frontStringAtLeast(mq.getBrokerName(), 32), + mq.getQueueId(), + offsetWrapper.getBrokerOffset(), + offsetWrapper.getConsumerOffset(), + diff, + inflight, + lastTime + ); + } + } + + System.out.printf("%n"); + System.out.printf("Consume TPS: %.2f%n", consumeStats.getConsumeTps()); + System.out.printf("Consume Diff Total: %d%n", diffTotal); + System.out.printf("Consume Inflight Total: %d%n", inflightTotal); + } else { + System.out.printf("%-64s %-6s %-24s %-5s %-14s %-7s %s%n", + "#Group", + "#Count", + "#Version", + "#Type", + "#Model", + "#TPS", + "#Diff Total" + ); + TopicList topicList = defaultMQAdminExt.fetchAllTopicList(); + for (String topic : topicList.getTopicList()) { + if (topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + String consumerGroup = KeyBuilder.parseGroup(topic); + try { + ConsumeStats consumeStats = null; + try { + consumeStats = defaultMQAdminExt.examineConsumeStats(consumerGroup); + } catch (Exception e) { + log.warn("examineConsumeStats exception, " + consumerGroup, e); + } + + ConsumerConnection cc = null; + try { + cc = defaultMQAdminExt.examineConsumerConnectionInfo(consumerGroup); + } catch (Exception e) { + log.warn("examineConsumerConnectionInfo exception, " + consumerGroup, e); + } + + GroupConsumeInfo groupConsumeInfo = new GroupConsumeInfo(); + groupConsumeInfo.setGroup(consumerGroup); + + if (consumeStats != null) { + groupConsumeInfo.setConsumeTps((int) consumeStats.getConsumeTps()); + groupConsumeInfo.setDiffTotal(consumeStats.computeTotalDiff()); + } + + if (cc != null) { + groupConsumeInfo.setCount(cc.getConnectionSet().size()); + groupConsumeInfo.setMessageModel(cc.getMessageModel()); + groupConsumeInfo.setConsumeType(cc.getConsumeType()); + groupConsumeInfo.setVersion(cc.computeMinVersion()); + } + + System.out.printf("%-64s %-6d %-24s %-5s %-14s %-7d %d%n", + UtilAll.frontStringAtLeast(groupConsumeInfo.getGroup(), 64), + groupConsumeInfo.getCount(), + groupConsumeInfo.getCount() > 0 ? groupConsumeInfo.versionDesc() : "OFFLINE", + groupConsumeInfo.consumeTypeDesc(), + groupConsumeInfo.messageModelDesc(), + groupConsumeInfo.getConsumeTps(), + groupConsumeInfo.getDiffTotal() + ); + } catch (Exception e) { + log.warn("examineConsumeStats or examineConsumerConnectionInfo exception, " + consumerGroup, e); + } + } + } + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} + +class GroupConsumeInfo implements Comparable { + private String group; + private int version; + private int count; + private ConsumeType consumeType; + private MessageModel messageModel; + private int consumeTps; + private long diffTotal; + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public String consumeTypeDesc() { + if (this.count != 0) { + return this.getConsumeType() == ConsumeType.CONSUME_ACTIVELY ? "PULL" : "PUSH"; + } + return ""; + } + + public ConsumeType getConsumeType() { + return consumeType; + } + + public void setConsumeType(ConsumeType consumeType) { + this.consumeType = consumeType; + } + + public String messageModelDesc() { + if (this.count != 0 && this.getConsumeType() == ConsumeType.CONSUME_PASSIVELY) { + return this.getMessageModel().toString(); + } + return ""; + } + + public MessageModel getMessageModel() { + return messageModel; + } + + public void setMessageModel(MessageModel messageModel) { + this.messageModel = messageModel; + } + + public String versionDesc() { + if (this.count != 0) { + return MQVersion.getVersionDesc(this.version); + } + return ""; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + + public long getDiffTotal() { + return diffTotal; + } + + public void setDiffTotal(long diffTotal) { + this.diffTotal = diffTotal; + } + + @Override + public int compareTo(GroupConsumeInfo o) { + if (this.count != o.count) { + return o.count - this.count; + } + + return (int) (o.diffTotal - diffTotal); + } + + public int getConsumeTps() { + return consumeTps; + } + + public void setConsumeTps(int consumeTps) { + this.consumeTps = consumeTps; + } + + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/ConsumerStatusSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/ConsumerStatusSubCommand.java new file mode 100644 index 0000000..d8f6f9a --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/ConsumerStatusSubCommand.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.tools.command.consumer; + +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.TreeMap; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.Connection; +import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.MQAdminStartup; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class ConsumerStatusSubCommand implements SubCommand { + + public static void main(String[] args) { + System.setProperty(MixAll.NAMESRV_ADDR_PROPERTY, "127.0.0.1:9876"); + MQAdminStartup.main(new String[] {new ConsumerStatusSubCommand().commandName(), "-g", "benchmark_consumer"}); + } + + @Override + public String commandName() { + return "consumerStatus"; + } + + @Override + public String commandDesc() { + return "Query consumer's internal data structure."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("g", "consumerGroup", true, "consumer group name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("i", "clientId", true, "The consumer's client id"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("b", "brokerAddr", true, "broker address"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("s", "jstack", false, "Run jstack command in the consumer progress"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + if (commandLine.hasOption('n')) { + defaultMQAdminExt.setNamesrvAddr(commandLine.getOptionValue('n').trim()); + } + + try { + defaultMQAdminExt.start(); + String group = commandLine.getOptionValue('g').trim(); + ConsumerConnection cc = commandLine.hasOption('b') + ? defaultMQAdminExt.examineConsumerConnectionInfo(group, commandLine.getOptionValue('b').trim()) + : defaultMQAdminExt.examineConsumerConnectionInfo(group); + boolean jstack = commandLine.hasOption('s'); + if (!commandLine.hasOption('i')) { + int i = 1; + long now = System.currentTimeMillis(); + final TreeMap criTable = new TreeMap<>(); + System.out.printf("%-10s %-40s %-20s %s%n", + "#Index", + "#ClientId", + "#Version", + "#ConsumerRunningInfoFile"); + for (Connection conn : cc.getConnectionSet()) { + try { + ConsumerRunningInfo consumerRunningInfo = + defaultMQAdminExt.getConsumerRunningInfo(group, conn.getClientId(), jstack); + if (consumerRunningInfo != null) { + criTable.put(conn.getClientId(), consumerRunningInfo); + String filePath = now + "/" + conn.getClientId(); + MixAll.string2FileNotSafe(consumerRunningInfo.formatString(), filePath); + System.out.printf("%-10d %-40s %-20s %s%n", + i++, + conn.getClientId(), + MQVersion.getVersionDesc(conn.getVersion()), + filePath); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + if (!criTable.isEmpty()) { + boolean subSame = ConsumerRunningInfo.analyzeSubscription(criTable); + + boolean rebalanceOK = subSame && ConsumerRunningInfo.analyzeRebalance(criTable); + + if (subSame) { + System.out.printf("%n%nSame subscription in the same group of consumer"); + System.out.printf("%n%nRebalance %s%n", rebalanceOK ? "OK" : "Failed"); + Iterator> it = criTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + String result = + ConsumerRunningInfo.analyzeProcessQueue(next.getKey(), next.getValue()); + if (result.length() > 0) { + System.out.printf("%s", result); + } + } + } else { + System.out.printf("%n%nWARN: Different subscription in the same group of consumer!!!"); + } + } + } else { + String clientId = commandLine.getOptionValue('i').trim(); + ConsumerRunningInfo consumerRunningInfo = + defaultMQAdminExt.getConsumerRunningInfo(group, clientId, jstack); + if (consumerRunningInfo != null) { + System.out.printf("%s", consumerRunningInfo.formatString()); + } + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/ConsumerSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/ConsumerSubCommand.java new file mode 100644 index 0000000..c9d29f1 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/ConsumerSubCommand.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.tools.command.consumer; + +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.TreeMap; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.Connection; +import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.MQAdminStartup; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class ConsumerSubCommand implements SubCommand { + + public static void main(String[] args) { + System.setProperty(MixAll.NAMESRV_ADDR_PROPERTY, "127.0.0.1:9876"); + MQAdminStartup.main(new String[] {new ConsumerSubCommand().commandName(), "-g", "benchmark_consumer"}); + } + + @Override + public String commandName() { + return "consumer"; + } + + @Override + public String commandDesc() { + return "Query consumer's connection, status, etc."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("g", "consumerGroup", true, "consumer group name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("s", "jstack", false, "Run jstack command in the consumer progress"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + defaultMQAdminExt.start(); + String group = commandLine.getOptionValue('g').trim(); + ConsumerConnection cc = defaultMQAdminExt.examineConsumerConnectionInfo(group); + boolean jstack = commandLine.hasOption('s'); + + if (!commandLine.hasOption('i')) { + + int i = 1; + long now = System.currentTimeMillis(); + final TreeMap criTable = + new TreeMap<>(); + for (Connection conn : cc.getConnectionSet()) { + try { + ConsumerRunningInfo consumerRunningInfo = + defaultMQAdminExt.getConsumerRunningInfo(group, conn.getClientId(), jstack); + if (consumerRunningInfo != null) { + criTable.put(conn.getClientId(), consumerRunningInfo); + String filePath = now + "/" + conn.getClientId(); + MixAll.string2FileNotSafe(consumerRunningInfo.formatString(), filePath); + System.out.printf("%03d %-40s %-20s %s%n", + i++, + conn.getClientId(), + MQVersion.getVersionDesc(conn.getVersion()), + filePath); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + if (!criTable.isEmpty()) { + boolean subSame = ConsumerRunningInfo.analyzeSubscription(criTable); + boolean rebalanceOK = subSame && ConsumerRunningInfo.analyzeRebalance(criTable); + + if (subSame) { + System.out.printf("%n%nSame subscription in the same group of consumer"); + System.out.printf("%n%nRebalance %s%n", rebalanceOK ? "OK" : "Failed"); + + Iterator> it = criTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + String result = + ConsumerRunningInfo.analyzeProcessQueue(next.getKey(), next.getValue()); + if (result.length() > 0) { + System.out.printf("%s", result); + } + } + } else { + System.out.printf("%n%nWARN: Different subscription in the same group of consumer!!!"); + } + } + } else { + String clientId = commandLine.getOptionValue('i').trim(); + ConsumerRunningInfo consumerRunningInfo = + defaultMQAdminExt.getConsumerRunningInfo(group, clientId, jstack); + if (consumerRunningInfo != null) { + System.out.printf("%s", consumerRunningInfo.formatString()); + } + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/DeleteSubscriptionGroupCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/DeleteSubscriptionGroupCommand.java new file mode 100644 index 0000000..44113e3 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/DeleteSubscriptionGroupCommand.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.tools.command.consumer; + +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class DeleteSubscriptionGroupCommand implements SubCommand { + @Override + public String commandName() { + return "deleteSubGroup"; + } + + @Override + public String commandDesc() { + return "Delete subscription group from broker."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("b", "brokerAddr", true, "delete subscription group from which broker"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "clusterName", true, "delete subscription group from which cluster"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("g", "groupName", true, "subscription group name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("r", "removeOffset", true, "remove offset"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt adminExt = new DefaultMQAdminExt(rpcHook); + adminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + try { + // groupName + String groupName = commandLine.getOptionValue('g').trim(); + boolean cleanOffset = false; + if (commandLine.hasOption('r')) { + try { + cleanOffset = Boolean.valueOf(commandLine.getOptionValue('r').trim()); + } catch (Exception e) { + } + } + + if (commandLine.hasOption('b')) { + String addr = commandLine.getOptionValue('b').trim(); + adminExt.start(); + + adminExt.deleteSubscriptionGroup(addr, groupName, cleanOffset); + System.out.printf("delete subscription group [%s] from broker [%s] success.%n", groupName, + addr); + + return; + } else if (commandLine.hasOption('c')) { + String clusterName = commandLine.getOptionValue('c').trim(); + adminExt.start(); + + Set masterSet = CommandUtil.fetchMasterAddrByClusterName(adminExt, clusterName); + for (String master : masterSet) { + adminExt.deleteSubscriptionGroup(master, groupName, cleanOffset); + System.out.printf( + "delete subscription group [%s] from broker [%s] in cluster [%s] success.%n", + groupName, master, clusterName); + } + + try { + adminExt.deleteTopic(MixAll.RETRY_GROUP_TOPIC_PREFIX + groupName, clusterName); + adminExt.deleteTopic(MixAll.DLQ_GROUP_TOPIC_PREFIX + groupName, clusterName); + } catch (Exception e) { + e.printStackTrace(); + } + return; + } + + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + adminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/GetConsumerConfigSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/GetConsumerConfigSubCommand.java new file mode 100644 index 0000000..4a8253a --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/GetConsumerConfigSubCommand.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.tools.command.consumer; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.collections.CollectionUtils; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class GetConsumerConfigSubCommand implements SubCommand { + + @Override + public String commandName() { + return "getConsumerConfig"; + } + + @Override + public String commandDesc() { + return "Get consumer config by subscription group name."; + } + + @Override + public Options buildCommandlineOptions(final Options options) { + Option opt = new Option("g", "groupName", true, "subscription group name"); + opt.setRequired(true); + options.addOption(opt); + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, + RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt adminExt = new DefaultMQAdminExt(rpcHook); + adminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + String groupName = commandLine.getOptionValue('g').trim(); + + if (commandLine.hasOption('n')) { + adminExt.setNamesrvAddr(commandLine.getOptionValue('n').trim()); + } + + try { + adminExt.start(); + List consumerConfigInfoList = new ArrayList<>(); + ClusterInfo clusterInfo = adminExt.examineBrokerClusterInfo(); + Map> clusterAddrTable = clusterInfo.getClusterAddrTable(); + for (Entry brokerEntry : clusterInfo.getBrokerAddrTable().entrySet()) { + String clusterName = this.getClusterName(brokerEntry.getKey(), clusterAddrTable); + String brokerAddress = brokerEntry.getValue().selectBrokerAddr(); + SubscriptionGroupConfig subscriptionGroupConfig = adminExt.examineSubscriptionGroupConfig(brokerAddress, groupName); + if (subscriptionGroupConfig == null) { + continue; + } + consumerConfigInfoList.add(new ConsumerConfigInfo(clusterName, brokerEntry.getKey(), subscriptionGroupConfig)); + } + if (CollectionUtils.isEmpty(consumerConfigInfoList)) { + return; + } + for (ConsumerConfigInfo info : consumerConfigInfoList) { + System.out.printf("=============================%s:%s=============================\n", + info.getClusterName(), info.getBrokerName()); + SubscriptionGroupConfig config = info.getSubscriptionGroupConfig(); + Field[] fields = config.getClass().getDeclaredFields(); + for (Field field : fields) { + field.setAccessible(true); + if (field.get(config) != null) { + System.out.printf("%s%-40s= %s\n", "", field.getName(), field.get(config).toString()); + } else { + System.out.printf("%s%-40s= %s\n", "", field.getName(), ""); + } + } + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + adminExt.shutdown(); + } + } + + private String getClusterName(String brokeName, Map> clusterAddrTable) { + for (Map.Entry> entry : clusterAddrTable.entrySet()) { + Set brokerNameSet = entry.getValue(); + if (brokerNameSet.contains(brokeName)) { + return entry.getKey(); + } + } + return null; + } +} + +class ConsumerConfigInfo { + private String clusterName; + + private String brokerName; + + private SubscriptionGroupConfig subscriptionGroupConfig; + + public ConsumerConfigInfo(String clusterName, String brokerName, SubscriptionGroupConfig subscriptionGroupConfig) { + this.clusterName = clusterName; + this.brokerName = brokerName; + this.subscriptionGroupConfig = subscriptionGroupConfig; + } + + 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 SubscriptionGroupConfig getSubscriptionGroupConfig() { + return subscriptionGroupConfig; + } + + public void setSubscriptionGroupConfig(SubscriptionGroupConfig subscriptionGroupConfig) { + this.subscriptionGroupConfig = subscriptionGroupConfig; + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/SetConsumeModeSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/SetConsumeModeSubCommand.java new file mode 100644 index 0000000..4390d0a --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/SetConsumeModeSubCommand.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.tools.command.consumer; + +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.message.MessageRequestMode; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + + +public class SetConsumeModeSubCommand implements SubCommand { + @Override + public String commandName() { + return "setConsumeMode"; + } + + + @Override + public String commandDesc() { + return "Set consume message mode. pull/pop etc."; + } + + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("b", "brokerAddr", true, "create subscription group to which broker"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "clusterName", true, "create subscription group to which cluster"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("t", "topicName", true, "topic name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("g", "groupName", true, "consumer group name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("m", "mode", true, "consume mode. PULL/POP"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("q", "popShareQueueNum", true, "num of queue which share in pop mode"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) + throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + defaultMQAdminExt.setVipChannelEnabled(false); + + try { + + String topicName = commandLine.getOptionValue('t').trim(); + String groupName = commandLine.getOptionValue('g').trim(); + + MessageRequestMode mode = MessageRequestMode.valueOf(commandLine.getOptionValue('m').trim()); + + + int popShareQueueNum = 0; + if (commandLine.hasOption('q')) { + popShareQueueNum = Integer.parseInt(commandLine.getOptionValue('q') + .trim()); + } + + + if (commandLine.hasOption('b')) { + String addr = commandLine.getOptionValue('b').trim(); + defaultMQAdminExt.start(); + + defaultMQAdminExt.setMessageRequestMode(addr, topicName, groupName, mode, popShareQueueNum, 5000); + System.out.printf("set consume mode to %s success.%n", addr); + System.out.printf("topic[%s] group[%s] consume mode[%s] popShareQueueNum[%d]", + topicName, groupName, mode.toString(), popShareQueueNum); + return; + + } else if (commandLine.hasOption('c')) { + String clusterName = commandLine.getOptionValue('c').trim(); + + defaultMQAdminExt.start(); + Set masterSet = + CommandUtil.fetchMasterAddrByClusterName(defaultMQAdminExt, clusterName); + for (String addr : masterSet) { + try { + defaultMQAdminExt.setMessageRequestMode(addr, topicName, groupName, mode, popShareQueueNum, 5000); + System.out.printf("set consume mode to %s success.%n", addr); + } catch (Exception e) { + e.printStackTrace(); + Thread.sleep(1000 * 1); + } + } + System.out.printf("topic[%s] group[%s] consume mode[%s] popShareQueueNum[%d]", + topicName, groupName, mode.toString(), popShareQueueNum); + return; + } + + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/StartMonitoringSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/StartMonitoringSubCommand.java new file mode 100644 index 0000000..f5e1404 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/StartMonitoringSubCommand.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.tools.command.consumer; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.apache.rocketmq.tools.monitor.DefaultMonitorListener; +import org.apache.rocketmq.tools.monitor.MonitorConfig; +import org.apache.rocketmq.tools.monitor.MonitorService; + +public class StartMonitoringSubCommand implements SubCommand { + + @Override + public String commandName() { + return "startMonitoring"; + } + + @Override + public String commandDesc() { + return "Start Monitoring."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + try { + MonitorService monitorService = + new MonitorService(new MonitorConfig(), new DefaultMonitorListener(), rpcHook); + + monitorService.start(); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/UpdateSubGroupListSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/UpdateSubGroupListSubCommand.java new file mode 100644 index 0000000..a36f50b --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/UpdateSubGroupListSubCommand.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.tools.command.consumer; + +import com.alibaba.fastjson2.JSON; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionGroup; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class UpdateSubGroupListSubCommand implements SubCommand { + @Override + public String commandName() { + return "updateSubGroupList"; + } + + @Override + public String commandDesc() { + return "Update or create subscription group in batch"; + } + + @Override + public Options buildCommandlineOptions(Options options) { + final OptionGroup optionGroup = new OptionGroup(); + Option opt = new Option("b", "brokerAddr", true, "create groups to which broker"); + optionGroup.addOption(opt); + + opt = new Option("c", "clusterName", true, "create groups to which cluster"); + optionGroup.addOption(opt); + optionGroup.setRequired(true); + options.addOptionGroup(optionGroup); + + opt = new Option("f", "filename", true, + "Path to a file with a list of org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig in json format"); + opt.setRequired(true); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, + RPCHook rpcHook) throws SubCommandException { + final DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + final String fileName = commandLine.getOptionValue('f').trim(); + + try { + final Path filePath = Paths.get(fileName); + if (!Files.exists(filePath)) { + System.out.printf("the file path %s does not exists%n", fileName); + return; + } + final byte[] groupConfigListBytes = Files.readAllBytes(filePath); + final List groupConfigs = JSON.parseArray(groupConfigListBytes, SubscriptionGroupConfig.class); + if (null == groupConfigs || groupConfigs.isEmpty()) { + return; + } + + if (commandLine.hasOption('b')) { + String brokerAddress = commandLine.getOptionValue('b').trim(); + defaultMQAdminExt.start(); + defaultMQAdminExt.createAndUpdateSubscriptionGroupConfigList(brokerAddress, groupConfigs); + + System.out.printf("submit batch of group config to %s success, please check the result later.%n", + brokerAddress); + return; + + } else if (commandLine.hasOption('c')) { + final String clusterName = commandLine.getOptionValue('c').trim(); + + defaultMQAdminExt.start(); + + Set masterSet = + CommandUtil.fetchMasterAddrByClusterName(defaultMQAdminExt, clusterName); + for (String brokerAddress : masterSet) { + defaultMQAdminExt.createAndUpdateSubscriptionGroupConfigList(brokerAddress, groupConfigs); + + System.out.printf("submit batch of subscription group config to %s success, please check the result later.%n", + brokerAddress); + } + } + + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/UpdateSubGroupSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/UpdateSubGroupSubCommand.java new file mode 100644 index 0000000..b17da4d --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/UpdateSubGroupSubCommand.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.tools.command.consumer; + +import com.alibaba.fastjson.JSON; +import java.util.Map; +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.attribute.AttributeParser; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.subscription.GroupRetryPolicy; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class UpdateSubGroupSubCommand implements SubCommand { + + @Override + public String commandName() { + return "updateSubGroup"; + } + + @Override + public String commandDesc() { + return "Update or create subscription group."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("b", "brokerAddr", true, "create subscription group to which broker"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "clusterName", true, "create subscription group to which cluster"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("g", "groupName", true, "consumer group name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("s", "consumeEnable", true, "consume enable"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("m", "consumeFromMinEnable", true, "from min offset"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("d", "consumeBroadcastEnable", true, "broadcast"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("o", "consumeMessageOrderly", true, "consume message orderly"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("q", "retryQueueNums", true, "retry queue nums"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("r", "retryMaxTimes", true, "retry max times"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("p", "groupRetryPolicy", true, + "the json string of retry policy ( exp: " + + "{\"type\":\"EXPONENTIAL\",\"exponentialRetryPolicy\":{\"initial\":5000,\"max\":7200000,\"multiplier\":2}} " + + "{\"type\":\"CUSTOMIZED\",\"customizedRetryPolicy\":{\"next\":[1000,5000,10000]}} )"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("i", "brokerId", true, "consumer from which broker id"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("w", "whichBrokerWhenConsumeSlowly", true, "which broker id when consume slowly"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("a", "notifyConsumerIdsChanged", true, "notify consumerId changed"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option(null, "attributes", true, "attribute(+a=b,+c=d,-e)"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(final CommandLine commandLine, final Options options, + RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setConsumeBroadcastEnable(false); + subscriptionGroupConfig.setConsumeFromMinEnable(false); + + // groupName + subscriptionGroupConfig.setGroupName(commandLine.getOptionValue('g').trim()); + + // consumeEnable + if (commandLine.hasOption('s')) { + subscriptionGroupConfig.setConsumeEnable(Boolean.parseBoolean(commandLine.getOptionValue('s') + .trim())); + } + + // consumeFromMinEnable + if (commandLine.hasOption('m')) { + subscriptionGroupConfig.setConsumeFromMinEnable(Boolean.parseBoolean(commandLine + .getOptionValue('m').trim())); + } + + // consumeBroadcastEnable + if (commandLine.hasOption('d')) { + subscriptionGroupConfig.setConsumeBroadcastEnable(Boolean.parseBoolean(commandLine + .getOptionValue('d').trim())); + } + + // consumeMessageOrderly + if (commandLine.hasOption('o')) { + subscriptionGroupConfig.setConsumeMessageOrderly(Boolean.parseBoolean(commandLine + .getOptionValue('o').trim())); + } + + // retryQueueNums + if (commandLine.hasOption('q')) { + subscriptionGroupConfig.setRetryQueueNums(Integer.parseInt(commandLine.getOptionValue('q') + .trim())); + } + + // retryMaxTimes + if (commandLine.hasOption('r')) { + subscriptionGroupConfig.setRetryMaxTimes(Integer.parseInt(commandLine.getOptionValue('r') + .trim())); + } + + // groupRetryPolicy + if (commandLine.hasOption('p')) { + String groupRetryPolicyJson = commandLine.getOptionValue('p').trim(); + GroupRetryPolicy groupRetryPolicy = JSON.parseObject(groupRetryPolicyJson, GroupRetryPolicy.class); + subscriptionGroupConfig.setGroupRetryPolicy(groupRetryPolicy); + } + + // brokerId + if (commandLine.hasOption('i')) { + subscriptionGroupConfig.setBrokerId(Long.parseLong(commandLine.getOptionValue('i').trim())); + } + + // whichBrokerWhenConsumeSlowly + if (commandLine.hasOption('w')) { + subscriptionGroupConfig.setWhichBrokerWhenConsumeSlowly(Long.parseLong(commandLine + .getOptionValue('w').trim())); + } + + // notifyConsumerIdsChanged + if (commandLine.hasOption('a')) { + subscriptionGroupConfig.setNotifyConsumerIdsChangedEnable(Boolean.parseBoolean(commandLine + .getOptionValue('a').trim())); + } + + if (commandLine.hasOption("attributes")) { + String attributesModification = commandLine.getOptionValue("attributes").trim(); + Map attributes = AttributeParser.parseToMap(attributesModification); + subscriptionGroupConfig.setAttributes(attributes); + } + + if (commandLine.hasOption('b')) { + String addr = commandLine.getOptionValue('b').trim(); + + defaultMQAdminExt.start(); + + defaultMQAdminExt.createAndUpdateSubscriptionGroupConfig(addr, subscriptionGroupConfig); + System.out.printf("create subscription group to %s success.%n", addr); + System.out.printf("%s", subscriptionGroupConfig); + return; + + } else if (commandLine.hasOption('c')) { + String clusterName = commandLine.getOptionValue('c').trim(); + + defaultMQAdminExt.start(); + Set masterSet = + CommandUtil.fetchMasterAddrByClusterName(defaultMQAdminExt, clusterName); + for (String addr : masterSet) { + try { + defaultMQAdminExt.createAndUpdateSubscriptionGroupConfig(addr, subscriptionGroupConfig); + System.out.printf("create subscription group to %s success.%n", addr); + } catch (Exception e) { + e.printStackTrace(); + Thread.sleep(1000 * 1); + } + } + System.out.printf("%s", subscriptionGroupConfig); + return; + } + + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/container/AddBrokerSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/container/AddBrokerSubCommand.java new file mode 100644 index 0000000..007d42a --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/container/AddBrokerSubCommand.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.tools.command.container; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class AddBrokerSubCommand implements SubCommand { + @Override + public String commandName() { + return "addBroker"; + } + + @Override + public String commandDesc() { + return "Add a broker to specified container."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("c", "brokerContainerAddr", true, "Broker container address"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("b", "brokerConfigPath", true, "Broker config path"); + opt.setRequired(true); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, + RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + defaultMQAdminExt.start(); + String brokerContainerAddr = commandLine.getOptionValue('c').trim(); + String brokerConfigPath = commandLine.getOptionValue('b').trim(); + defaultMQAdminExt.addBrokerToContainer(brokerContainerAddr, brokerConfigPath); + System.out.printf("add broker to %s success%n", brokerContainerAddr); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/container/RemoveBrokerSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/container/RemoveBrokerSubCommand.java new file mode 100644 index 0000000..ab25d8e --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/container/RemoveBrokerSubCommand.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.tools.command.container; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class RemoveBrokerSubCommand implements SubCommand { + @Override + public String commandName() { + return "removeBroker"; + } + + @Override + public String commandDesc() { + return "Remove a broker from specified container."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("c", "brokerContainerAddr", true, "Broker container address"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("b", "brokerIdentity", true, "Information to identify a broker: clusterName:brokerName:brokerId(dLedgerId for dLedger)"); + opt.setRequired(true); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, + RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + defaultMQAdminExt.start(); + String brokerContainerAddr = commandLine.getOptionValue('c').trim(); + String[] brokerIdentities = commandLine.getOptionValue('b').trim().split(":"); + String clusterName = brokerIdentities[0].trim(); + String brokerName = brokerIdentities[1].trim(); + long brokerId; + try { + brokerId = Long.parseLong(brokerIdentities[2].trim()); + } catch (NumberFormatException e) { + e.printStackTrace(); + return; + } + if (brokerId < 0) { + System.out.printf("brokerId can't be negative%n"); + return; + } + defaultMQAdminExt.removeBrokerFromContainer(brokerContainerAddr, clusterName, brokerName, brokerId); + System.out.printf("remove broker from %s success%n", brokerContainerAddr); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/controller/CleanControllerBrokerMetaSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/controller/CleanControllerBrokerMetaSubCommand.java new file mode 100644 index 0000000..24ed025 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/controller/CleanControllerBrokerMetaSubCommand.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.tools.command.controller; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +import java.util.Arrays; + +public class CleanControllerBrokerMetaSubCommand implements SubCommand { + + @Override + public String commandName() { + return "cleanBrokerMetadata"; + } + + @Override + public String commandDesc() { + return "Clean metadata of broker on controller."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + + Option opt = new Option("a", "controllerAddress", true, "The address of controller"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("b", "brokerControllerIdsToClean", true, "The brokerController id list which requires to clean metadata. eg: 1;2;3, means that clean broker-1, broker-2 and broker-3"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("bn", "brokerName", true, "The broker name of the replicas that require to be manipulated"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("c", "clusterName", true, "The clusterName of broker"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("l", "cleanLivingBroker", false, "Whether clean up living brokers,default value is false"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + String controllerAddress = commandLine.getOptionValue('a').trim(); + String brokerName = commandLine.getOptionValue("bn").trim(); + String clusterName = null; + String brokerControllerIdsToClean = null; + + if (commandLine.hasOption('c')) { + clusterName = commandLine.getOptionValue('c').trim(); + } + if (commandLine.hasOption('b')) { + brokerControllerIdsToClean = commandLine.getOptionValue('b').trim(); + try { + Arrays.stream(brokerControllerIdsToClean.split(";")).map(idStr -> Long.parseLong(idStr)); + } catch (NumberFormatException numberFormatException) { + throw new IllegalArgumentException("please set the option according to the format", numberFormatException); + } + } + boolean isCleanLivingBroker = false; + if (commandLine.hasOption('l')) { + isCleanLivingBroker = true; + } + + if (!isCleanLivingBroker && StringUtils.isEmpty(clusterName)) { + throw new IllegalArgumentException("cleanLivingBroker option is false, clusterName option can not be empty."); + } + + try { + defaultMQAdminExt.start(); + defaultMQAdminExt.cleanControllerBrokerData(controllerAddress, clusterName, brokerName, brokerControllerIdsToClean, isCleanLivingBroker); + System.out.printf("clear broker %s metadata from controller success! \n", brokerName); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/controller/GetControllerConfigSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/controller/GetControllerConfigSubCommand.java new file mode 100644 index 0000000..ca3aa0d --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/controller/GetControllerConfigSubCommand.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.tools.command.controller; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class GetControllerConfigSubCommand implements SubCommand { + @Override + public String commandName() { + return "getControllerConfig"; + } + + @Override + public String commandDesc() { + return "Get controller config."; + } + + @Override + public Options buildCommandlineOptions(final Options options) { + Option opt = new Option("a", "controllerAddress", true, "Controller address list, eg: 192.168.0.1:9878;192.168.0.2:9878"); + opt.setRequired(true); + options.addOption(opt); + + return options; + } + + @Override + public void execute(final CommandLine commandLine, final Options options, + final RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + try { + // servers + String servers = commandLine.getOptionValue('a'); + List serverList = null; + if (servers != null && servers.length() > 0) { + String[] serverArray = servers.trim().split(";"); + + if (serverArray.length > 0) { + serverList = Arrays.asList(serverArray); + } + } + + defaultMQAdminExt.start(); + + Map controllerConfigs = defaultMQAdminExt.getControllerConfig(serverList); + + for (Map.Entry controllerConfigEntry : controllerConfigs.entrySet()) { + System.out.printf("============%s============\n", + controllerConfigEntry.getKey()); + for (Map.Entry entry : controllerConfigEntry.getValue().entrySet()) { + System.out.printf("%-50s= %s\n", entry.getKey(), entry.getValue()); + } + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/controller/GetControllerMetaDataSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/controller/GetControllerMetaDataSubCommand.java new file mode 100644 index 0000000..9664431 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/controller/GetControllerMetaDataSubCommand.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.tools.command.controller; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.header.controller.GetMetaDataResponseHeader; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class GetControllerMetaDataSubCommand implements SubCommand { + @Override + public String commandName() { + return "getControllerMetaData"; + } + + @Override + public String commandDesc() { + return "Get controller cluster's metadata."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("a", "controllerAddress", true, "the address of controller"); + opt.setRequired(true); + options.addOption(opt); + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + String controllerAddress = commandLine.getOptionValue('a').trim(); + try { + defaultMQAdminExt.start(); + final GetMetaDataResponseHeader metaData = defaultMQAdminExt.getControllerMetaData(controllerAddress); + System.out.printf("\n#ControllerGroup\t%s", metaData.getGroup()); + System.out.printf("\n#ControllerLeaderId\t%s", metaData.getControllerLeaderId()); + System.out.printf("\n#ControllerLeaderAddress\t%s", metaData.getControllerLeaderAddress()); + final String peers = metaData.getPeers(); + if (StringUtils.isNotEmpty(peers)) { + final String[] peerList = peers.split(";"); + for (String peer : peerList) { + System.out.printf("\n#Peer:\t%s", peer); + } + } + System.out.printf("\n"); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/controller/ReElectMasterSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/controller/ReElectMasterSubCommand.java new file mode 100644 index 0000000..a522a90 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/controller/ReElectMasterSubCommand.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.tools.command.controller; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.BrokerMemberGroup; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterResponseHeader; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class ReElectMasterSubCommand implements SubCommand { + + @Override + public String commandName() { + return "electMaster"; + } + + @Override + public String commandDesc() { + return "Re-elect the specified broker as master."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("a", "controllerAddress", true, "The address of controller"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("b", "brokerId", true, "The id of the broker which requires to become master"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("bn", "brokerName", true, "The broker name of the replicas that require to be manipulated"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("c", "clusterName", true, "the clusterName of broker"); + opt.setRequired(true); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + String controllerAddress = commandLine.getOptionValue("a").trim(); + String clusterName = commandLine.getOptionValue('c').trim(); + String brokerName = commandLine.getOptionValue("bn").trim(); + Long brokerId = Long.valueOf(commandLine.getOptionValue("b").trim()); + + try { + defaultMQAdminExt.start(); + final Pair pair = defaultMQAdminExt.electMaster(controllerAddress, clusterName, brokerName, brokerId); + final ElectMasterResponseHeader metaData = pair.getObject1(); + final BrokerMemberGroup brokerMemberGroup = pair.getObject2(); + System.out.printf("\n#ClusterName\t%s", clusterName); + System.out.printf("\n#BrokerName\t%s", brokerName); + System.out.printf("\n#BrokerMasterAddr\t%s", metaData.getMasterAddress()); + System.out.printf("\n#MasterEpoch\t%s", metaData.getMasterEpoch()); + System.out.printf("\n#SyncStateSetEpoch\t%s\n", metaData.getSyncStateSetEpoch()); + if (null != brokerMemberGroup && null != brokerMemberGroup.getBrokerAddrs()) { + brokerMemberGroup.getBrokerAddrs().forEach((key, value) -> System.out.printf("\t#Broker\t%d\t%s\n", key, value)); + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/controller/UpdateControllerConfigSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/controller/UpdateControllerConfigSubCommand.java new file mode 100644 index 0000000..90e1f5e --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/controller/UpdateControllerConfigSubCommand.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.tools.command.controller; + +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class UpdateControllerConfigSubCommand implements SubCommand { + @Override + public String commandName() { + return "updateControllerConfig"; + } + + @Override + public String commandDesc() { + return "Update controller config."; + } + + @Override + public Options buildCommandlineOptions(final Options options) { + Option opt = new Option("a", "controllerAddress", true, "Controller address list, eg: 192.168.0.1:9878;192.168.0.2:9878"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("k", "key", true, "config key"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("v", "value", true, "config value"); + opt.setRequired(true); + options.addOption(opt); + + return options; + } + + @Override + public void execute(final CommandLine commandLine, final Options options, + final RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + try { + // key name + String key = commandLine.getOptionValue('k').trim(); + // key name + String value = commandLine.getOptionValue('v').trim(); + Properties properties = new Properties(); + properties.put(key, value); + + // servers + String servers = commandLine.getOptionValue('a'); + List serverList = null; + if (servers != null && servers.length() > 0) { + String[] serverArray = servers.trim().split(";"); + + if (serverArray.length > 0) { + serverList = Arrays.asList(serverArray); + } + } + + defaultMQAdminExt.start(); + + defaultMQAdminExt.updateControllerConfig(properties, serverList); + + System.out.printf("update controller config success!%s\n%s : %s\n", + serverList == null ? "" : serverList, key, value); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/export/ExportConfigsCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/export/ExportConfigsCommand.java new file mode 100644 index 0000000..c3f96d5 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/export/ExportConfigsCommand.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.tools.command.export; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Arrays; +import java.util.Properties; + +import com.alibaba.fastjson.JSON; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class ExportConfigsCommand implements SubCommand { + @Override + public String commandName() { + return "exportConfigs"; + } + + @Override + public String commandDesc() { + return "Export configs."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("c", "clusterName", true, "choose a cluster to export"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("f", "filePath", true, + "export configs.json path | default /tmp/rocketmq/export"); + opt.setRequired(false); + options.addOption(opt); + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) + throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + String clusterName = commandLine.getOptionValue('c').trim(); + String filePath = !commandLine.hasOption('f') ? "/tmp/rocketmq/export" : commandLine.getOptionValue('f') + .trim(); + + defaultMQAdminExt.start(); + Map result = new HashMap<>(); + // name servers + List nameServerAddressList = defaultMQAdminExt.getNameServerAddressList(); + + //broker + int masterBrokerSize = 0; + int slaveBrokerSize = 0; + Map brokerConfigs = new HashMap<>(); + Map> masterAndSlaveMap + = CommandUtil.fetchMasterAndSlaveDistinguish(defaultMQAdminExt, clusterName); + for (Entry> masterAndSlaveEntry : masterAndSlaveMap.entrySet()) { + Properties masterProperties = defaultMQAdminExt.getBrokerConfig(masterAndSlaveEntry.getKey()); + masterBrokerSize++; + slaveBrokerSize += masterAndSlaveEntry.getValue().size(); + + brokerConfigs.put(masterProperties.getProperty("brokerName"), needBrokerProprties(masterProperties)); + } + + Map clusterScaleMap = new HashMap<>(); + clusterScaleMap.put("namesrvSize", nameServerAddressList.size()); + clusterScaleMap.put("masterBrokerSize", masterBrokerSize); + clusterScaleMap.put("slaveBrokerSize", slaveBrokerSize); + + result.put("brokerConfigs", brokerConfigs); + result.put("clusterScale", clusterScaleMap); + + String path = filePath + "/configs.json"; + MixAll.string2FileNotSafe(JSON.toJSONString(result, true), path); + System.out.printf("export %s success", path); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } + + + private Properties needBrokerProprties(Properties properties) { + List propertyKeys = Arrays.asList( + "brokerClusterName", + "brokerId", + "brokerName", + "brokerRole", + "fileReservedTime", + "filterServerNums", + "flushDiskType", + "maxMessageSize", + "messageDelayLevel", + "msgTraceTopicName", + "slaveReadEnable", + "traceOn", + "traceTopicEnable", + "useTLS", + "autoCreateTopicEnable", + "autoCreateSubscriptionGroup" + ); + + Properties newProperties = new Properties(); + propertyKeys.stream() + .filter(key -> properties.getProperty(key) != null) + .forEach(key -> newProperties.setProperty(key, properties.getProperty(key))); + + return newProperties; + } + +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/export/ExportMetadataCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/export/ExportMetadataCommand.java new file mode 100644 index 0000000..748f7b1 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/export/ExportMetadataCommand.java @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tools.command.export; + +import com.alibaba.fastjson.JSON; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.SubscriptionGroupWrapper; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class ExportMetadataCommand implements SubCommand { + + private static final String DEFAULT_FILE_PATH = "/tmp/rocketmq/export"; + + @Override + public String commandName() { + return "exportMetadata"; + } + + @Override + public String commandDesc() { + return "Export metadata."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("c", "clusterName", true, "choose a cluster to export"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("b", "brokerAddr", true, "choose a broker to export"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("f", "filePath", true, "export metadata.json path | default /tmp/rocketmq/export"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("t", "topic", false, "only export topic metadata"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("g", "subscriptionGroup", false, "only export subscriptionGroup metadata"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("s", "specialTopic", false, "need retryTopic and dlqTopic"); + opt.setRequired(false); + options.addOption(opt); + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) + throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + defaultMQAdminExt.start(); + + String filePath = !commandLine.hasOption('f') ? DEFAULT_FILE_PATH : commandLine.getOptionValue('f') + .trim(); + + boolean specialTopic = commandLine.hasOption('s'); + + if (commandLine.hasOption('b')) { + final String brokerAddr = commandLine.getOptionValue('b').trim(); + + if (commandLine.hasOption('t')) { + filePath = filePath + "/topic.json"; + TopicConfigSerializeWrapper topicConfigSerializeWrapper = defaultMQAdminExt.getUserTopicConfig( + brokerAddr, specialTopic, 10000L); + MixAll.string2FileNotSafe(JSON.toJSONString(topicConfigSerializeWrapper, true), filePath); + System.out.printf("export %s success", filePath); + } else if (commandLine.hasOption('g')) { + filePath = filePath + "/subscriptionGroup.json"; + SubscriptionGroupWrapper subscriptionGroupWrapper = defaultMQAdminExt.getUserSubscriptionGroup( + brokerAddr, 10000L); + MixAll.string2FileNotSafe(JSON.toJSONString(subscriptionGroupWrapper, true), filePath); + System.out.printf("export %s success", filePath); + } + } else if (commandLine.hasOption('c')) { + String clusterName = commandLine.getOptionValue('c').trim(); + + Set masterSet = + CommandUtil.fetchMasterAddrByClusterName(defaultMQAdminExt, clusterName); + + Map topicConfigMap = new HashMap<>(); + Map subGroupConfigMap = new HashMap<>(); + Map result = new HashMap<>(); + + for (String addr : masterSet) { + TopicConfigSerializeWrapper topicConfigSerializeWrapper = defaultMQAdminExt.getUserTopicConfig( + addr, specialTopic, 10000L); + + SubscriptionGroupWrapper subscriptionGroupWrapper = defaultMQAdminExt.getUserSubscriptionGroup( + addr, 10000); + + for (Map.Entry entry : topicConfigSerializeWrapper.getTopicConfigTable() + .entrySet()) { + TopicConfig topicConfig = topicConfigMap.get(entry.getKey()); + if (null != topicConfig) { + entry.getValue().setWriteQueueNums( + topicConfig.getWriteQueueNums() + entry.getValue().getWriteQueueNums()); + entry.getValue().setReadQueueNums( + topicConfig.getReadQueueNums() + entry.getValue().getReadQueueNums()); + } + topicConfigMap.put(entry.getKey(), entry.getValue()); + } + + for (Map.Entry entry : subscriptionGroupWrapper.getSubscriptionGroupTable() + .entrySet()) { + + SubscriptionGroupConfig subscriptionGroupConfig = subGroupConfigMap.get(entry.getKey()); + if (null != subscriptionGroupConfig) { + entry.getValue().setRetryQueueNums( + subscriptionGroupConfig.getRetryQueueNums() + entry.getValue().getRetryQueueNums()); + } + subGroupConfigMap.put(entry.getKey(), entry.getValue()); + } + + } + + String exportPath; + if (commandLine.hasOption('t')) { + result.put("topicConfigTable", topicConfigMap); + exportPath = filePath + "/topic.json"; + } else if (commandLine.hasOption('g')) { + result.put("subscriptionGroupTable", subGroupConfigMap); + exportPath = filePath + "/subscriptionGroup.json"; + } else { + result.put("topicConfigTable", topicConfigMap); + result.put("subscriptionGroupTable", subGroupConfigMap); + exportPath = filePath + "/metadata.json"; + } + result.put("exportTime", System.currentTimeMillis()); + MixAll.string2FileNotSafe(JSON.toJSONString(result, true), exportPath); + System.out.printf("export %s success%n", exportPath); + + } else { + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} + diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/export/ExportMetadataInRocksDBCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/export/ExportMetadataInRocksDBCommand.java new file mode 100644 index 0000000..438d17d --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/export/ExportMetadataInRocksDBCommand.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.tools.command.export; + +import com.alibaba.fastjson.JSONObject; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BiConsumer; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.config.ConfigRocksDBStorage; +import org.apache.rocketmq.common.utils.DataConverter; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.rocksdb.RocksIterator; + +public class ExportMetadataInRocksDBCommand implements SubCommand { + private static final String TOPICS_JSON_CONFIG = "topics"; + private static final String SUBSCRIPTION_GROUP_JSON_CONFIG = "subscriptionGroups"; + + @Override + public String commandName() { + return "exportMetadataInRocksDB"; + } + + @Override + public String commandDesc() { + return "export RocksDB kv config (topics/subscriptionGroups). Recommend to use [mqadmin rocksDBConfigToJson]"; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option pathOption = new Option("p", "path", true, + "Absolute path for the metadata directory"); + pathOption.setRequired(true); + options.addOption(pathOption); + + Option configTypeOption = new Option("t", "configType", true, "Name of kv config, e.g. " + + "topics/subscriptionGroups"); + configTypeOption.setRequired(true); + options.addOption(configTypeOption); + + Option jsonEnableOption = new Option("j", "jsonEnable", true, + "Json format enable, Default: false"); + jsonEnableOption.setRequired(false); + options.addOption(jsonEnableOption); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + String path = commandLine.getOptionValue("path").trim(); + if (StringUtils.isEmpty(path) || !UtilAll.isPathExists(path)) { + System.out.print("RocksDB path is invalid.\n"); + return; + } + + String configType = commandLine.getOptionValue("configType").trim(); + if (!path.endsWith("/")) { + path += "/"; + } + path += configType; + + boolean jsonEnable = false; + if (commandLine.hasOption("jsonEnable")) { + jsonEnable = Boolean.parseBoolean(commandLine.getOptionValue("jsonEnable").trim()); + } + + + ConfigRocksDBStorage kvStore = new ConfigRocksDBStorage(path, true /* readOnly */); + if (!kvStore.start()) { + System.out.printf("RocksDB load error, path=%s\n" , path); + return; + } + + try { + if (TOPICS_JSON_CONFIG.equalsIgnoreCase(configType) || SUBSCRIPTION_GROUP_JSON_CONFIG.equalsIgnoreCase(configType)) { + handleExportMetadata(kvStore, configType, jsonEnable); + } else { + System.out.printf("Invalid config type=%s, Options: topics,subscriptionGroups\n", configType); + } + } finally { + kvStore.shutdown(); + } + } + + private static void handleExportMetadata(ConfigRocksDBStorage kvStore, String configType, boolean jsonEnable) { + if (jsonEnable) { + final Map jsonConfig = new HashMap<>(); + final Map configTable = new HashMap<>(); + iterateKvStore(kvStore, (key, value) -> { + final String configKey = new String(key, DataConverter.CHARSET_UTF8); + final String configValue = new String(value, DataConverter.CHARSET_UTF8); + final JSONObject jsonObject = JSONObject.parseObject(configValue); + configTable.put(configKey, jsonObject); + } + ); + + jsonConfig.put(configType.equalsIgnoreCase(TOPICS_JSON_CONFIG) ? "topicConfigTable" : "subscriptionGroupTable", + (JSONObject) JSONObject.toJSON(configTable)); + final String jsonConfigStr = JSONObject.toJSONString(jsonConfig, true); + System.out.print(jsonConfigStr + "\n"); + } else { + AtomicLong count = new AtomicLong(0); + iterateKvStore(kvStore, (key, value) -> { + final String configKey = new String(key, DataConverter.CHARSET_UTF8); + final String configValue = new String(value, DataConverter.CHARSET_UTF8); + System.out.printf("%d, Key: %s, Value: %s%n", count.incrementAndGet(), configKey, configValue); + }); + } + } + + private static void iterateKvStore(ConfigRocksDBStorage kvStore, BiConsumer biConsumer) { + try (RocksIterator iterator = kvStore.iterator()) { + iterator.seekToFirst(); + for (iterator.seekToFirst(); iterator.isValid(); iterator.next()) { + biConsumer.accept(iterator.key(), iterator.value()); + } + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/export/ExportMetricsCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/export/ExportMetricsCommand.java new file mode 100644 index 0000000..5d8bb37 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/export/ExportMetricsCommand.java @@ -0,0 +1,280 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tools.command.export; + +import com.alibaba.fastjson.JSON; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +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.MixAll; +import org.apache.rocketmq.common.stats.Stats; +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.body.BrokerStatsData; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.body.Connection; +import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection; +import org.apache.rocketmq.remoting.protocol.body.KVTable; +import org.apache.rocketmq.remoting.protocol.body.SubscriptionGroupWrapper; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.apache.rocketmq.tools.command.stats.StatsAllSubCommand; + +public class ExportMetricsCommand implements SubCommand { + + @Override + public String commandName() { + return "exportMetrics"; + } + + @Override + public String commandDesc() { + return "Export metrics."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("c", "clusterName", true, "choose a cluster to export"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("f", "filePath", true, + "export metrics.json path | default /tmp/rocketmq/export"); + opt.setRequired(false); + options.addOption(opt); + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) + throws SubCommandException { + + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + String clusterName = commandLine.getOptionValue('c').trim(); + String filePath = !commandLine.hasOption('f') ? "/tmp/rocketmq/export" : commandLine.getOptionValue('f') + .trim(); + + defaultMQAdminExt.start(); + + Map>> evaluateReportMap = new HashMap<>(); + Map totalTpsMap = new HashMap<>(); + Map totalOneDayNumMap = new HashMap<>(); + initTotalMap(totalTpsMap, totalOneDayNumMap); + + ClusterInfo clusterInfoSerializeWrapper = defaultMQAdminExt.examineBrokerClusterInfo(); + Set brokerNameSet = clusterInfoSerializeWrapper.getClusterAddrTable().get(clusterName); + for (String brokerName : brokerNameSet) { + BrokerData brokerData = clusterInfoSerializeWrapper.getBrokerAddrTable().get(brokerName); + if (brokerData != null) { + String addr = brokerData.getBrokerAddrs().get(0L); + + KVTable kvTable = defaultMQAdminExt.fetchBrokerRuntimeStats(addr); + + Properties properties = defaultMQAdminExt.getBrokerConfig(addr); + + SubscriptionGroupWrapper subscriptionGroupWrapper = defaultMQAdminExt.getUserSubscriptionGroup(addr, + 10000); + + Map> brokerInfo = new HashMap<>(); + + //broker environment,machine configuration + brokerInfo.put("runtimeEnv", getRuntimeEnv(kvTable, properties)); + + brokerInfo.put("runtimeQuota", + getRuntimeQuota(kvTable, defaultMQAdminExt, addr, totalTpsMap, + totalOneDayNumMap, subscriptionGroupWrapper)); + + // runtime version + brokerInfo.put("runtimeVersion", + getRuntimeVersion(defaultMQAdminExt, subscriptionGroupWrapper)); + + evaluateReportMap.put(brokerName, brokerInfo); + } + + } + + String path = filePath + "/metrics.json"; + + Map totalData = new HashMap<>(); + totalData.put("totalTps", totalTpsMap); + totalData.put("totalOneDayNum", totalOneDayNumMap); + + Map result = new HashMap<>(); + result.put("evaluateReport", evaluateReportMap); + result.put("totalData", totalData); + + MixAll.string2FileNotSafe(JSON.toJSONString(result, true), path); + System.out.printf("export %s success", path); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + + } + + private Map getRuntimeVersion(DefaultMQAdminExt defaultMQAdminExt, + SubscriptionGroupWrapper subscriptionGroupWrapper) { + Map runtimeVersionMap = new HashMap(); + + Set clientInfoSet = new HashSet<>(); + for (Map.Entry entry : subscriptionGroupWrapper + .getSubscriptionGroupTable().entrySet()) { + try { + ConsumerConnection cc = defaultMQAdminExt.examineConsumerConnectionInfo( + entry.getValue().getGroupName()); + for (Connection conn : cc.getConnectionSet()) { + String clientInfo = conn.getLanguage() + "%" + MQVersion.getVersionDesc(conn.getVersion()); + clientInfoSet.add(clientInfo); + } + } catch (Exception e) { + continue; + } + } + runtimeVersionMap.put("rocketmqVersion", MQVersion.getVersionDesc(MQVersion.CURRENT_VERSION)); + runtimeVersionMap.put("clientInfo", clientInfoSet); + return runtimeVersionMap; + } + + private Map getRuntimeEnv(KVTable kvTable, Properties properties) { + Map runtimeEnvMap = new HashMap<>(); + runtimeEnvMap.put("cpuNum", properties.getProperty("clientCallbackExecutorThreads")); + runtimeEnvMap.put("totalMemKBytes", kvTable.getTable().get("totalMemKBytes")); + return runtimeEnvMap; + } + + private Map getRuntimeQuota(KVTable kvTable, DefaultMQAdminExt defaultMQAdminExt, String brokerAddr, + Map totalTpsMap, Map totalOneDayNumMap, + SubscriptionGroupWrapper subscriptionGroupWrapper) + throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + TopicConfigSerializeWrapper topicConfigSerializeWrapper = defaultMQAdminExt.getUserTopicConfig( + brokerAddr, false, 10000); + + BrokerStatsData transStatsData = null; + + try { + transStatsData = defaultMQAdminExt.viewBrokerStatsData(brokerAddr, + Stats.TOPIC_PUT_NUMS, + TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC); + } catch (MQClientException e) { + } + + BrokerStatsData scheduleStatsData = null; + try { + scheduleStatsData = defaultMQAdminExt.viewBrokerStatsData(brokerAddr, + Stats.TOPIC_PUT_NUMS, TopicValidator.RMQ_SYS_SCHEDULE_TOPIC); + } catch (MQClientException e) { + } + + Map runtimeQuotaMap = new HashMap<>(); + //disk use ratio + Map diskRatioMap = new HashMap<>(); + diskRatioMap.put("commitLogDiskRatio", kvTable.getTable().get("commitLogDiskRatio")); + diskRatioMap.put("consumeQueueDiskRatio", kvTable.getTable().get("consumeQueueDiskRatio")); + runtimeQuotaMap.put("diskRatio", diskRatioMap); + + //inTps and outTps + Map tpsMap = new HashMap<>(); + double normalInTps = 0; + double normalOutTps = 0; + String putTps = kvTable.getTable().get("putTps"); + String getTransferredTps = kvTable.getTable().get("getTransferredTps"); + String[] inTpss = putTps.split(" "); + if (inTpss.length > 0) { + normalInTps = Double.parseDouble(inTpss[0]); + } + + String[] outTpss = getTransferredTps.split(" "); + if (outTpss.length > 0) { + normalOutTps = Double.parseDouble(outTpss[0]); + } + + double transInTps = null != transStatsData ? transStatsData.getStatsMinute().getTps() : 0.0; + double scheduleInTps = null != scheduleStatsData ? scheduleStatsData.getStatsMinute().getTps() : 0.0; + + long transOneDayInNum = null != transStatsData ? StatsAllSubCommand.compute24HourSum(transStatsData) : 0; + long scheduleOneDayInNum = null != scheduleStatsData ? StatsAllSubCommand.compute24HourSum(scheduleStatsData) : 0; + + //current minute tps + tpsMap.put("normalInTps", normalInTps); + tpsMap.put("normalOutTps", normalOutTps); + tpsMap.put("transInTps", transInTps); + tpsMap.put("scheduleInTps", scheduleInTps); + runtimeQuotaMap.put("tps", tpsMap); + + //one day num + Map oneDayNumMap = new HashMap<>(); + long normalOneDayInNum = Long.parseLong(kvTable.getTable().get("msgPutTotalTodayMorning")) - + Long.parseLong(kvTable.getTable().get("msgPutTotalYesterdayMorning")); + long normalOneDayOutNum = Long.parseLong(kvTable.getTable().get("msgGetTotalTodayMorning")) - + Long.parseLong(kvTable.getTable().get("msgGetTotalYesterdayMorning")); + oneDayNumMap.put("normalOneDayInNum", normalOneDayInNum); + oneDayNumMap.put("normalOneDayOutNum", normalOneDayOutNum); + oneDayNumMap.put("transOneDayInNum", transOneDayInNum); + oneDayNumMap.put("scheduleOneDayInNum", scheduleOneDayInNum); + runtimeQuotaMap.put("oneDayNum", oneDayNumMap); + + //all broker current minute tps + totalTpsMap.put("totalNormalInTps", totalTpsMap.get("totalNormalInTps") + normalInTps); + totalTpsMap.put("totalNormalOutTps", totalTpsMap.get("totalNormalOutTps") + normalOutTps); + totalTpsMap.put("totalTransInTps", totalTpsMap.get("totalTransInTps") + transInTps); + totalTpsMap.put("totalScheduleInTps", totalTpsMap.get("totalScheduleInTps") + scheduleInTps); + + + //all broker one day num + totalOneDayNumMap.put("normalOneDayInNum", totalOneDayNumMap.get("normalOneDayInNum") + normalOneDayInNum); + totalOneDayNumMap.put("normalOneDayOutNum", totalOneDayNumMap.get("normalOneDayOutNum") + normalOneDayOutNum); + totalOneDayNumMap.put("transOneDayInNum", totalOneDayNumMap.get("transOneDayInNum") + transOneDayInNum); + totalOneDayNumMap.put("scheduleOneDayInNum", totalOneDayNumMap.get("scheduleOneDayInNum") + scheduleOneDayInNum); + + // putMessageAverageSize + runtimeQuotaMap.put("messageAverageSize", kvTable.getTable().get("putMessageAverageSize")); + + //topicSize + runtimeQuotaMap.put("topicSize", topicConfigSerializeWrapper.getTopicConfigTable().size()); + runtimeQuotaMap.put("groupSize", subscriptionGroupWrapper.getSubscriptionGroupTable().size()); + return runtimeQuotaMap; + } + + private void initTotalMap(Map totalTpsMap, Map totalOneDayNumMap) { + totalTpsMap.put("totalNormalInTps", 0.0); + totalTpsMap.put("totalNormalOutTps", 0.0); + totalTpsMap.put("totalTransInTps", 0.0); + totalTpsMap.put("totalScheduleInTps", 0.0); + + totalOneDayNumMap.put("normalOneDayInNum", 0L); + totalOneDayNumMap.put("normalOneDayOutNum", 0L); + totalOneDayNumMap.put("transOneDayInNum", 0L); + totalOneDayNumMap.put("scheduleOneDayInNum", 0L); + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/export/ExportPopRecordCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/export/ExportPopRecordCommand.java new file mode 100644 index 0000000..f8b67c9 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/export/ExportPopRecordCommand.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.tools.command.export; + +import java.util.Set; +import java.util.concurrent.TimeUnit; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class ExportPopRecordCommand implements SubCommand { + + @Override + public String commandName() { + return "exportPopRecord"; + } + + @Override + public String commandDesc() { + return "Export pop consumer record"; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option( + "c", "clusterName", true, "choose one cluster to export"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("b", "brokerAddr", true, "choose one broker to export"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("d", "dryRun", true, "no actual changes will be made"); + opt.setRequired(false); + options.addOption(opt); + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + + DefaultMQAdminExt adminExt = new DefaultMQAdminExt(rpcHook); + adminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + adminExt.start(); + boolean dryRun = commandLine.hasOption('d') && + Boolean.FALSE.toString().equalsIgnoreCase(commandLine.getOptionValue('d')); + if (commandLine.hasOption('b')) { + String brokerAddr = commandLine.getOptionValue('b').trim(); + String brokerName = adminExt.getBrokerConfig(brokerAddr).getProperty("brokerName"); + export(adminExt, brokerAddr, brokerName, dryRun); + } else if (commandLine.hasOption('c')) { + String clusterName = commandLine.getOptionValue('c').trim(); + ClusterInfo clusterInfo = adminExt.examineBrokerClusterInfo(); + if (clusterInfo != null) { + Set brokerNameSet = clusterInfo.getClusterAddrTable().get(clusterName); + if (brokerNameSet != null) { + brokerNameSet.forEach(brokerName -> { + BrokerData brokerData = clusterInfo.getBrokerAddrTable().get(brokerName); + if (brokerData != null) { + brokerData.getBrokerAddrs().forEach( + (brokerId, brokerAddr) -> export(adminExt, brokerAddr, brokerName, dryRun)); + } + }); + } + } + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + adminExt.shutdown(); + } + } + + private void export(DefaultMQAdminExt adminExt, String brokerAddr, String brokerName, boolean dryRun) { + try { + if (!dryRun) { + adminExt.exportPopRecords(brokerAddr, TimeUnit.SECONDS.toMillis(30)); + } + System.out.printf("Export broker records, " + + "brokerName=%s, brokerAddr=%s, dryRun=%s%n", brokerName, brokerAddr, dryRun); + } catch (Exception e) { + System.out.printf("Export broker records error, " + + "brokerName=%s, brokerAddr=%s, dryRun=%s%n%s", brokerName, brokerAddr, dryRun, e); + } + } +} + diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/ha/GetSyncStateSetSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/ha/GetSyncStateSetSubCommand.java new file mode 100644 index 0000000..b6231e4 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/ha/GetSyncStateSetSubCommand.java @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tools.command.ha; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.BrokerReplicasInfo; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class GetSyncStateSetSubCommand implements SubCommand { + @Override + public String commandName() { + return "getSyncStateSet"; + } + + @Override + public String commandDesc() { + return "Fetch syncStateSet for target brokers."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("a", "controllerAddress", true, "the address of controller"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("c", "clusterName", true, "which cluster"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("b", "brokerName", true, "which broker to fetch"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("i", "interval", true, "the interval(second) of get info"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + if (commandLine.hasOption('i')) { + String interval = commandLine.getOptionValue('i'); + int flushSecond = 3; + if (interval != null && !interval.trim().equals("")) { + flushSecond = Integer.parseInt(interval); + } + + defaultMQAdminExt.start(); + + while (true) { + this.innerExec(commandLine, options, defaultMQAdminExt); + Thread.sleep(flushSecond * 1000); + } + } else { + defaultMQAdminExt.start(); + + this.innerExec(commandLine, options, defaultMQAdminExt); + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } + + private void innerExec(CommandLine commandLine, Options options, + DefaultMQAdminExt defaultMQAdminExt) throws Exception { + String controllerAddress = commandLine.getOptionValue('a').trim().split(";")[0]; + if (commandLine.hasOption('b')) { + String brokerName = commandLine.getOptionValue('b').trim(); + final ArrayList brokers = new ArrayList<>(); + brokers.add(brokerName); + printData(controllerAddress, brokers, defaultMQAdminExt); + } else if (commandLine.hasOption('c')) { + String clusterName = commandLine.getOptionValue('c').trim(); + Set brokerNames = CommandUtil.fetchBrokerNameByClusterName(defaultMQAdminExt, clusterName); + printData(controllerAddress, new ArrayList<>(brokerNames), defaultMQAdminExt); + } else { + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } + } + + private void printData(String controllerAddress, List brokerNames, + DefaultMQAdminExt defaultMQAdminExt) throws Exception { + if (brokerNames.size() > 0) { + final BrokerReplicasInfo brokerReplicasInfo = defaultMQAdminExt.getInSyncStateData(controllerAddress, brokerNames); + final Map replicasInfoTable = brokerReplicasInfo.getReplicasInfoTable(); + for (Map.Entry next : replicasInfoTable.entrySet()) { + final List inSyncReplicas = next.getValue().getInSyncReplicas(); + final List notInSyncReplicas = next.getValue().getNotInSyncReplicas(); + System.out.printf("\n#brokerName\t%s\n#MasterBrokerId\t%d\n#MasterAddr\t%s\n#MasterEpoch\t%d\n#SyncStateSetEpoch\t%d\n#SyncStateSetNums\t%d\n", + next.getKey(), next.getValue().getMasterBrokerId(), next.getValue().getMasterAddress(), next.getValue().getMasterEpoch(), next.getValue().getSyncStateSetEpoch(), + inSyncReplicas.size()); + for (BrokerReplicasInfo.ReplicaIdentity member : inSyncReplicas) { + System.out.printf("\nInSyncReplica:\t%s\n", member.toString()); + } + + for (BrokerReplicasInfo.ReplicaIdentity member : notInSyncReplicas) { + System.out.printf("\nNotInSyncReplica:\t%s\n", member.toString()); + } + } + } + } +} + diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/ha/HAStatusSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/ha/HAStatusSubCommand.java new file mode 100644 index 0000000..931658a --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/ha/HAStatusSubCommand.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.tools.command.ha; + +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.HARuntimeInfo; +import org.apache.rocketmq.remoting.protocol.body.HARuntimeInfo.HAClientRuntimeInfo; +import org.apache.rocketmq.remoting.protocol.body.HARuntimeInfo.HAConnectionRuntimeInfo; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class HAStatusSubCommand implements SubCommand { + + @Override + public String commandName() { + return "haStatus"; + } + + @Override + public String commandDesc() { + return "Fetch ha runtime status data."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("c", "clusterName", true, "which cluster"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("b", "brokerAddr", true, "which broker to fetch"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("i", "interval", true, "the interval(second) of get info"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + if (commandLine.hasOption('i')) { + String interval = commandLine.getOptionValue('i'); + int flushSecond = 3; + if (interval != null && !interval.trim().equals("")) { + flushSecond = Integer.parseInt(interval); + } + + defaultMQAdminExt.start(); + + while (true) { + this.innerExec(commandLine, options, defaultMQAdminExt); + Thread.sleep(flushSecond * 1000); + } + } else { + defaultMQAdminExt.start(); + + this.innerExec(commandLine, options, defaultMQAdminExt); + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } + + private void innerExec(CommandLine commandLine, Options options, + DefaultMQAdminExt defaultMQAdminExt) throws Exception { + if (commandLine.hasOption('b')) { + String addr = commandLine.getOptionValue('b').trim(); + this.printStatus(addr, defaultMQAdminExt); + } else if (commandLine.hasOption('c')) { + + String clusterName = commandLine.getOptionValue('c').trim(); + Set masterSet = CommandUtil.fetchMasterAddrByClusterName(defaultMQAdminExt, clusterName); + + for (String addr : masterSet) { + this.printStatus(addr, defaultMQAdminExt); + } + } else { + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } + + } + + private void printStatus(String brokerAddr, DefaultMQAdminExt defaultMQAdminExt) throws Exception { + HARuntimeInfo haRuntimeInfo = defaultMQAdminExt.getBrokerHAStatus(brokerAddr); + + if (haRuntimeInfo.isMaster()) { + System.out.printf("\n#MasterAddr\t%s\n#MasterCommitLogMaxOffset\t%d\n#SlaveNum\t%d\n#InSyncSlaveNum\t%d\n", brokerAddr, + haRuntimeInfo.getMasterCommitLogMaxOffset(), haRuntimeInfo.getHaConnectionInfo().size(), haRuntimeInfo.getInSyncSlaveNums()); + System.out.printf("%-32s %-16s %16s %16s %16s %16s\n", + "#SlaveAddr", + "#SlaveAckOffset", + "#Diff", + "#TransferSpeed(KB/s)", + "#Status", + "#TransferFromWhere" + ); + + for (HAConnectionRuntimeInfo cInfo : haRuntimeInfo.getHaConnectionInfo()) { + System.out.printf("%-32s %-16d %16d %16.2f %16s %16d\n", + cInfo.getAddr(), + cInfo.getSlaveAckOffset(), + cInfo.getDiff(), + cInfo.getTransferredByteInSecond() / 1024.0, + cInfo.isInSync() ? "OK" : "Fall Behind", + cInfo.getTransferFromWhere()); + } + } else { + HAClientRuntimeInfo haClientRuntimeInfo = haRuntimeInfo.getHaClientRuntimeInfo(); + + System.out.printf("\n#MasterAddr\t%s\n", haClientRuntimeInfo.getMasterAddr()); + System.out.printf("#CommitLogMaxOffset\t%d\n", haClientRuntimeInfo.getMaxOffset()); + System.out.printf("#TransferSpeed(KB/s)\t%.2f\n", haClientRuntimeInfo.getTransferredByteInSecond() / 1024.0); + System.out.printf("#LastReadTime\t%s\n", UtilAll.timeMillisToHumanString2(haClientRuntimeInfo.getLastReadTimestamp())); + System.out.printf("#LastWriteTime\t%s\n", UtilAll.timeMillisToHumanString2(haClientRuntimeInfo.getLastWriteTimestamp())); + System.out.printf("#MasterFlushOffset\t%s\n", haClientRuntimeInfo.getMasterFlushOffset()); + } + } + +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/message/CheckMsgSendRTCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/message/CheckMsgSendRTCommand.java new file mode 100644 index 0000000..b15b59d --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/message/CheckMsgSendRTCommand.java @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tools.command.message; + +import java.util.List; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.MessageQueueSelector; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class CheckMsgSendRTCommand implements SubCommand { + private static String brokerName = ""; + private static int queueId = 0; + + @Override + public String commandName() { + return "checkMsgSendRT"; + } + + @Override + public String commandDesc() { + return "Check message send response time."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("t", "topic", true, "topic name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("a", "amount", true, "message amount | default 100"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("s", "size", true, "message size | default 128 Byte"); + opt.setRequired(false); + options.addOption(opt); + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQProducer producer = new DefaultMQProducer(rpcHook); + producer.setProducerGroup(Long.toString(System.currentTimeMillis())); + + try { + producer.start(); + long start = 0; + long end = 0; + long timeElapsed = 0; + boolean sendSuccess = false; + String topic = commandLine.getOptionValue('t').trim(); + long amount = !commandLine.hasOption('a') ? 100 : Long.parseLong(commandLine + .getOptionValue('a').trim()); + long msgSize = !commandLine.hasOption('s') ? 128 : Long.parseLong(commandLine + .getOptionValue('s').trim()); + Message msg = new Message(topic, getStringBySize(msgSize).getBytes(MixAll.DEFAULT_CHARSET)); + + System.out.printf("%-32s %-4s %-20s %s%n", + "#Broker Name", + "#QID", + "#Send Result", + "#RT" + ); + for (int i = 0; i < amount; i++) { + start = System.currentTimeMillis(); + try { + producer.send(msg, new MessageQueueSelector() { + @Override + public MessageQueue select(List mqs, Message msg, Object arg) { + int queueIndex = (Integer) arg % mqs.size(); + MessageQueue queue = mqs.get(queueIndex); + brokerName = queue.getBrokerName(); + queueId = queue.getQueueId(); + return queue; + } + }, i); + sendSuccess = true; + end = System.currentTimeMillis(); + } catch (Exception e) { + sendSuccess = false; + end = System.currentTimeMillis(); + } + + if (i != 0) { + timeElapsed += end - start; + } + + System.out.printf("%-32s %-4s %-20s %s%n", + brokerName, + queueId, + sendSuccess, + end - start + ); + } + + double rt = (double) timeElapsed / (amount - 1); + System.out.printf("Avg RT: %s%n", String.format("%.2f", rt)); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + producer.shutdown(); + } + } + + public String getStringBySize(long size) { + StringBuilder res = new StringBuilder(); + for (int i = 0; i < size; i++) { + res.append('a'); + } + return res.toString(); + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/message/ConsumeMessageCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/message/ConsumeMessageCommand.java new file mode 100644 index 0000000..02ff532 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/message/ConsumeMessageCommand.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.tools.command.message; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +import java.util.Set; + +public class ConsumeMessageCommand implements SubCommand { + + private String topic = null; + private long messageCount = 128; + private DefaultMQPullConsumer defaultMQPullConsumer; + + + public enum ConsumeType { + /** + * Topic only + */ + DEFAULT, + /** + * Topic brokerName queueId set + */ + BYQUEUE, + /** + * Topic brokerName queueId offset set + */ + BYOFFSET + } + + private static long timestampFormat(final String value) { + long timestamp; + try { + timestamp = Long.parseLong(value); + } catch (NumberFormatException e) { + timestamp = UtilAll.parseDate(value, UtilAll.YYYY_MM_DD_HH_MM_SS_SSS).getTime(); + } + + return timestamp; + } + @Override + public String commandName() { + return "consumeMessage"; + } + + @Override + public String commandDesc() { + return "Consume message."; + } + + @Override + public Options buildCommandlineOptions(final Options options) { + Option opt = new Option("t", "topic", true, "Topic name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("b", "brokerName", true, "Broker name"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("i", "queueId", true, "Queue Id"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("o", "offset", true, "Queue offset"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("g", "consumerGroup", true, "Consumer group name"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("s", "beginTimestamp ", true, + "Begin timestamp[currentTimeMillis|yyyy-MM-dd#HH:mm:ss:SSS]"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("e", "endTimestamp ", true, + "End timestamp[currentTimeMillis|yyyy-MM-dd#HH:mm:ss:SSS]"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "MessageNumber", true, "Number of message to be consumed"); + opt.setRequired(false); + options.addOption(opt); + + + return options; + + } + + @Override + public void execute(final CommandLine commandLine, final Options options, RPCHook rpcHook) throws SubCommandException { + if (defaultMQPullConsumer == null) { + defaultMQPullConsumer = new DefaultMQPullConsumer(MixAll.TOOLS_CONSUMER_GROUP, rpcHook); + } + defaultMQPullConsumer.setInstanceName(Long.toString(System.currentTimeMillis())); + + long offset = 0; + long timeValueEnd = 0; + long timeValueBegin = 0; + String queueId = null; + String brokerName = null; + ConsumeType consumeType = ConsumeType.DEFAULT; + + try { + /* Group name must be set before consumer start */ + if (commandLine.hasOption('g')) { + String consumerGroup = commandLine.getOptionValue('g').trim(); + defaultMQPullConsumer.setConsumerGroup(consumerGroup); + } + + defaultMQPullConsumer.start(); + + topic = commandLine.getOptionValue('t').trim(); + + if (commandLine.hasOption('c')) { + messageCount = Long.parseLong(commandLine.getOptionValue('c').trim()); + if (messageCount <= 0) { + System.out.print("Please input a positive messageNumber!"); + return; + } + } + if (commandLine.hasOption('b')) { + brokerName = commandLine.getOptionValue('b').trim(); + + } + if (commandLine.hasOption('i')) { + if (!commandLine.hasOption('b')) { + System.out.print("Please set the brokerName before queueId!"); + return; + } + queueId = commandLine.getOptionValue('i').trim(); + + consumeType = ConsumeType.BYQUEUE; + } + if (commandLine.hasOption('o')) { + if (consumeType != ConsumeType.BYQUEUE) { + System.out.print("Please set queueId before offset!"); + return; + } + offset = Long.parseLong(commandLine.getOptionValue('o').trim()); + consumeType = ConsumeType.BYOFFSET; + } + + long now = System.currentTimeMillis(); + if (commandLine.hasOption('s')) { + String timestampStr = commandLine.getOptionValue('s').trim(); + timeValueBegin = timestampFormat(timestampStr); + if (timeValueBegin > now) { + System.out.print("Please set the beginTimestamp before now!"); + return; + } + } + if (commandLine.hasOption('e')) { + String timestampStr = commandLine.getOptionValue('e').trim(); + timeValueEnd = timestampFormat(timestampStr); + if (timeValueEnd > now) { + System.out.print("Please set the endTimestamp before now!"); + return; + } + if (timeValueBegin > timeValueEnd) { + System.out.print("Please make sure that the beginTimestamp is less than or equal to the endTimestamp"); + return; + } + } + + switch (consumeType) { + case DEFAULT: + executeDefault(timeValueBegin, timeValueEnd); + break; + case BYOFFSET: + executeByCondition(brokerName, queueId, offset, timeValueBegin, timeValueEnd); + break; + case BYQUEUE: + executeByCondition(brokerName, queueId, 0, timeValueBegin, timeValueEnd); + break; + default: + System.out.print("Unknown type of consume!"); + break; + } + + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQPullConsumer.shutdown(); + } + } + + private void pullMessageByQueue(MessageQueue mq, long minOffset, long maxOffset) { + READQ: + for (long offset = minOffset; offset <= maxOffset; ) { + PullResult pullResult = null; + try { + pullResult = defaultMQPullConsumer.pull(mq, "*", offset, (int)(maxOffset - offset + 1)); + } catch (Exception e) { + e.printStackTrace(); + return; + } + if (pullResult != null) { + offset = pullResult.getNextBeginOffset(); + switch (pullResult.getPullStatus()) { + case FOUND: + System.out.print("Consume ok\n"); + PrintMessageByQueueCommand.printMessage(pullResult.getMsgFoundList(), "UTF-8", + true, true); + break; + case NO_MATCHED_MSG: + System.out.printf("%s no matched msg. status=%s, offset=%s\n", mq, pullResult.getPullStatus(), + offset); + break; + case NO_NEW_MSG: + case OFFSET_ILLEGAL: + System.out.printf("%s print msg finished. status=%s, offset=%s\n", mq, + pullResult.getPullStatus(), offset); + break READQ; + default: + break; + } + } + } + } + + private void executeDefault(long timeValueBegin, long timeValueEnd) { + try { + Set mqs = defaultMQPullConsumer.fetchSubscribeMessageQueues(topic); + long countLeft = messageCount; + for (MessageQueue mq : mqs) { + if (countLeft == 0) { + return; + } + long minOffset = defaultMQPullConsumer.minOffset(mq); + long maxOffset = defaultMQPullConsumer.maxOffset(mq); + if (timeValueBegin > 0) { + minOffset = defaultMQPullConsumer.searchOffset(mq, timeValueBegin); + } + if (timeValueEnd > 0) { + maxOffset = defaultMQPullConsumer.searchOffset(mq, timeValueEnd); + } + if (maxOffset - minOffset > countLeft) { + System.out.printf("The older %d message of the %d queue will be provided\n", countLeft, mq.getQueueId()); + maxOffset = minOffset + countLeft - 1; + countLeft = 0; + } else { + countLeft = countLeft - (maxOffset - minOffset) - 1; + } + + pullMessageByQueue(mq, minOffset, maxOffset); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void executeByCondition(String brokerName, String queueId, long offset, long timeValueBegin, long timeValueEnd) { + MessageQueue mq = new MessageQueue(topic, brokerName, Integer.parseInt(queueId)); + try { + long minOffset = defaultMQPullConsumer.minOffset(mq); + long maxOffset = defaultMQPullConsumer.maxOffset(mq); + if (timeValueBegin > 0) { + minOffset = defaultMQPullConsumer.searchOffset(mq, timeValueBegin); + } + if (timeValueEnd > 0) { + maxOffset = defaultMQPullConsumer.searchOffset(mq, timeValueEnd); + } + if (offset > maxOffset) { + System.out.printf("%s no matched msg, offset=%s\n", mq, offset); + return; + } + minOffset = minOffset > offset ? minOffset : offset; + if (maxOffset - minOffset > messageCount) { + System.out.printf("The oldler %d message will be provided\n", messageCount); + maxOffset = minOffset + messageCount - 1; + } + + pullMessageByQueue(mq, minOffset, maxOffset); + } catch (Exception e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/message/DecodeMessageIdCommond.java b/tools/src/main/java/org/apache/rocketmq/tools/command/message/DecodeMessageIdCommond.java new file mode 100644 index 0000000..532508c --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/message/DecodeMessageIdCommond.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.tools.command.message; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class DecodeMessageIdCommond implements SubCommand { + @Override + public String commandName() { + return "DecodeMessageId"; + } + + @Override + public String commandDesc() { + return "decode unique message ID"; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("i", "messageId", true, "unique message ID"); + opt.setRequired(false); + options.addOption(opt); + return options; + } + + @Override + public void execute(final CommandLine commandLine, final Options options, + RPCHook rpcHook) throws SubCommandException { + String messageId = commandLine.getOptionValue('i').trim(); + + try { + System.out.printf("ip=%s", MessageClientIDSetter.getIPStrFromID(messageId)); + } catch (Exception e) { + e.printStackTrace(); + } + + try { + String date = UtilAll.formatDate(MessageClientIDSetter.getNearlyTimeFromID(messageId), UtilAll.YYYY_MM_DD_HH_MM_SS_SSS); + System.out.printf("date=%s", date); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/message/DumpCompactionLogCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/message/DumpCompactionLogCommand.java new file mode 100644 index 0000000..eee8f3d --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/message/DumpCompactionLogCommand.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.tools.command.message; + +import org.apache.commons.cli.Options; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +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; + +public class DumpCompactionLogCommand implements SubCommand { + @Override + public String commandDesc() { + return "Parse compaction log to message."; + } + + @Override + public String commandName() { + return "dumpCompactionLog"; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("f", "file", true, "to dump file name"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) + throws SubCommandException { + if (commandLine.hasOption("f")) { + String fileName = commandLine.getOptionValue("f"); + Path filePath = Paths.get(fileName); + if (!Files.exists(filePath)) { + throw new SubCommandException("file " + fileName + " not exist."); + } + + if (Files.isDirectory(filePath)) { + throw new SubCommandException("file " + fileName + " is a directory."); + } + + try { + long fileSize = Files.size(filePath); + FileChannel fileChannel = new RandomAccessFile(fileName, "rw").getChannel(); + ByteBuffer buf = fileChannel.map(MapMode.READ_WRITE, 0, fileSize); + + int current = 0; + while (current < fileSize) { + buf.position(current); + ByteBuffer bb = buf.slice(); + int size = bb.getInt(); + if (size > buf.capacity() || size < 0) { + break; + } else { + bb.limit(size); + bb.rewind(); + } + + try { + MessageExt messageExt = MessageDecoder.decode(bb, false, false); + if (messageExt == null) { + break; + } else { + current += size; + System.out.printf(messageExt + "\n"); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + UtilAll.cleanBuffer(buf); + } catch (IOException e) { + e.printStackTrace(); + } + + } else { + System.out.print("miss dump log file name\n"); + } + + + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/message/PrintMessageByQueueCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/message/PrintMessageByQueueCommand.java new file mode 100644 index 0000000..0418e88 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/message/PrintMessageByQueueCommand.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.tools.command.message; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class PrintMessageByQueueCommand implements SubCommand { + + public static long timestampFormat(final String value) { + long timestamp = 0; + try { + timestamp = Long.parseLong(value); + } catch (NumberFormatException e) { + + timestamp = UtilAll.parseDate(value, UtilAll.YYYY_MM_DD_HH_MM_SS_SSS).getTime(); + } + + return timestamp; + } + + private static void calculateByTag(final List msgs, final Map tagCalmap, + final boolean calByTag) { + if (!calByTag) + return; + + for (MessageExt msg : msgs) { + String tag = msg.getTags(); + if (StringUtils.isNotBlank(tag)) { + AtomicLong count = tagCalmap.get(tag); + if (count == null) { + count = new AtomicLong(); + tagCalmap.put(tag, count); + } + count.incrementAndGet(); + } + } + } + + private static void printCalculateByTag(final Map tagCalmap, final boolean calByTag) { + if (!calByTag) + return; + + List list = new ArrayList<>(); + for (Map.Entry entry : tagCalmap.entrySet()) { + TagCountBean tagBean = new TagCountBean(entry.getKey(), entry.getValue()); + list.add(tagBean); + } + Collections.sort(list); + + for (TagCountBean tagCountBean : list) { + System.out.printf("Tag: %-30s Count: %s%n", tagCountBean.getTag(), tagCountBean.getCount()); + } + } + + public static void printMessage(final List msgs, final String charsetName, boolean printMsg, + boolean printBody) { + if (!printMsg) + return; + + for (MessageExt msg : msgs) { + try { + System.out.printf("MSGID: %s %s BODY: %s%n", msg.getMsgId(), msg, + printBody ? new String(msg.getBody(), charsetName) : "NOT PRINT BODY"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + } + } + + @Override + public String commandName() { + return "printMsgByQueue"; + } + + @Override + public String commandDesc() { + return "Print Message Detail by queueId."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("t", "topic", true, "topic name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("a", "brokerName ", true, "broker name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("i", "queueId ", true, "queue id"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("c", "charsetName ", true, "CharsetName(eg: UTF-8,GBK)"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("s", "subExpression ", true, "Subscribe Expression(eg: TagA || TagB)"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("b", "beginTimestamp ", true, "Begin timestamp[currentTimeMillis|yyyy-MM-dd#HH:mm:ss:SSS]"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("e", "endTimestamp ", true, "End timestamp[currentTimeMillis|yyyy-MM-dd#HH:mm:ss:SSS]"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("p", "print msg", true, "print msg. eg: true | false(default)"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("d", "printBody ", true, "print body. eg: true | false(default)"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("f", "calculate", true, "calculate by tag. eg: true | false(default)"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQPullConsumer consumer = new DefaultMQPullConsumer(MixAll.TOOLS_CONSUMER_GROUP, rpcHook); + + try { + String charsetName = + !commandLine.hasOption('c') ? "UTF-8" : commandLine.getOptionValue('c').trim(); + boolean printMsg = + commandLine.hasOption('p') && Boolean.parseBoolean(commandLine.getOptionValue('p').trim()); + boolean printBody = + commandLine.hasOption('d') && Boolean.parseBoolean(commandLine.getOptionValue('d').trim()); + boolean calByTag = + commandLine.hasOption('f') && Boolean.parseBoolean(commandLine.getOptionValue('f').trim()); + String subExpression = + !commandLine.hasOption('s') ? "*" : commandLine.getOptionValue('s').trim(); + + String topic = commandLine.getOptionValue('t').trim(); + String brokerName = commandLine.getOptionValue('a').trim(); + int queueId = Integer.parseInt(commandLine.getOptionValue('i').trim()); + consumer.start(); + + MessageQueue mq = new MessageQueue(topic, brokerName, queueId); + long minOffset = consumer.minOffset(mq); + long maxOffset = consumer.maxOffset(mq); + + if (commandLine.hasOption('b')) { + String timestampStr = commandLine.getOptionValue('b').trim(); + long timeValue = timestampFormat(timestampStr); + minOffset = consumer.searchOffset(mq, timeValue); + } + + if (commandLine.hasOption('e')) { + String timestampStr = commandLine.getOptionValue('e').trim(); + long timeValue = timestampFormat(timestampStr); + maxOffset = consumer.searchOffset(mq, timeValue); + } + + final Map tagCalmap = new HashMap<>(); + READQ: + for (long offset = minOffset; offset < maxOffset; ) { + try { + PullResult pullResult = consumer.pull(mq, subExpression, offset, 32); + offset = pullResult.getNextBeginOffset(); + switch (pullResult.getPullStatus()) { + case FOUND: + calculateByTag(pullResult.getMsgFoundList(), tagCalmap, calByTag); + printMessage(pullResult.getMsgFoundList(), charsetName, printMsg, printBody); + break; + case NO_MATCHED_MSG: + case NO_NEW_MSG: + case OFFSET_ILLEGAL: + break READQ; + } + } catch (Exception e) { + e.printStackTrace(); + break; + } + } + + printCalculateByTag(tagCalmap, calByTag); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + consumer.shutdown(); + } + } + + static class TagCountBean implements Comparable { + private String tag; + private AtomicLong count; + + public TagCountBean(final String tag, final AtomicLong count) { + this.tag = tag; + this.count = count; + } + + public String getTag() { + return tag; + } + + public void setTag(final String tag) { + this.tag = tag; + } + + public AtomicLong getCount() { + return count; + } + + public void setCount(final AtomicLong count) { + this.count = count; + } + + @Override + public int compareTo(final TagCountBean o) { + return (int) (o.getCount().get() - this.count.get()); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/message/PrintMessageSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/message/PrintMessageSubCommand.java new file mode 100644 index 0000000..97e101d --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/message/PrintMessageSubCommand.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.tools.command.message; + +import java.io.UnsupportedEncodingException; +import java.util.List; +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.impl.FindBrokerResult; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class PrintMessageSubCommand implements SubCommand { + + public static long timestampFormat(final String value) { + long timestamp = 0; + try { + timestamp = Long.parseLong(value); + } catch (NumberFormatException e) { + timestamp = UtilAll.parseDate(value, UtilAll.YYYY_MM_DD_HH_MM_SS_SSS).getTime(); + } + + return timestamp; + } + + public static void printMessage(final List msgs, final String charsetName, boolean printBody) { + for (MessageExt msg : msgs) { + try { + System.out.printf("MSGID: %s %s BODY: %s%n", msg.getMsgId(), msg.toString(), + printBody ? new String(msg.getBody(), charsetName) : "NOT PRINT BODY"); + } catch (UnsupportedEncodingException e) { + } + } + } + + @Override + public String commandName() { + return "printMsg"; + } + + @Override + public String commandDesc() { + return "Print Message Detail."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("t", "topic", true, "topic name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("c", "charsetName ", true, "CharsetName(eg: UTF-8,GBK)"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("s", "subExpression ", true, "Subscribe Expression(eg: TagA || TagB)"); + opt.setRequired(false); + options.addOption(opt); + + opt = + new Option("b", "beginTimestamp ", true, + "Begin timestamp[currentTimeMillis|yyyy-MM-dd#HH:mm:ss:SSS]"); + opt.setRequired(false); + options.addOption(opt); + + opt = + new Option("e", "endTimestamp ", true, + "End timestamp[currentTimeMillis|yyyy-MM-dd#HH:mm:ss:SSS]"); + opt.setRequired(false); + options.addOption(opt); + + opt = + new Option("d", "printBody ", true, + "print body"); + opt.setRequired(false); + options.addOption(opt); + + opt = + new Option("l", "lmqParentTopic", true, + "Lmq parent topic, lmq is used to find the route."); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQPullConsumer consumer = new DefaultMQPullConsumer(MixAll.TOOLS_CONSUMER_GROUP, rpcHook); + + try { + String topic = commandLine.getOptionValue('t').trim(); + + String charsetName = + !commandLine.hasOption('c') ? "UTF-8" : commandLine.getOptionValue('c').trim(); + + String subExpression = + !commandLine.hasOption('s') ? "*" : commandLine.getOptionValue('s').trim(); + + String lmqParentTopic = + !commandLine.hasOption('l') ? null : commandLine.getOptionValue('l').trim(); + + boolean printBody = !commandLine.hasOption('d') || Boolean.parseBoolean(commandLine.getOptionValue('d').trim()); + + consumer.start(); + + Set mqs; + if (lmqParentTopic != null) { + mqs = consumer.fetchSubscribeMessageQueues(lmqParentTopic); + mqs.forEach(mq -> mq.setTopic(topic)); + } else { + mqs = consumer.fetchSubscribeMessageQueues(topic); + } + for (MessageQueue mq : mqs) { + long minOffset = consumer.minOffset(mq); + long maxOffset = consumer.maxOffset(mq); + + if (commandLine.hasOption('b')) { + String timestampStr = commandLine.getOptionValue('b').trim(); + long timeValue = timestampFormat(timestampStr); + minOffset = consumer.searchOffset(mq, timeValue); + } + + if (commandLine.hasOption('e')) { + String timestampStr = commandLine.getOptionValue('e').trim(); + long timeValue = timestampFormat(timestampStr); + maxOffset = consumer.searchOffset(mq, timeValue); + } + + System.out.printf("minOffset=%s, maxOffset=%s, %s%n", minOffset, maxOffset, mq); + + READQ: + for (long offset = minOffset; offset < maxOffset; ) { + try { + fillBrokerAddrIfNotExist(consumer, mq, lmqParentTopic); + PullResult pullResult = consumer.pull(mq, subExpression, offset, 32); + offset = pullResult.getNextBeginOffset(); + switch (pullResult.getPullStatus()) { + case FOUND: + printMessage(pullResult.getMsgFoundList(), charsetName, printBody); + break; + case NO_MATCHED_MSG: + System.out.printf("%s no matched msg. status=%s, offset=%s%n", mq, pullResult.getPullStatus(), offset); + break; + case NO_NEW_MSG: + case OFFSET_ILLEGAL: + System.out.printf("%s print msg finished. status=%s, offset=%s%n", mq, pullResult.getPullStatus(), offset); + break READQ; + } + } catch (Exception e) { + e.printStackTrace(); + break; + } + } + System.out.printf("--------------------------------------------------------\n"); + } + + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + consumer.shutdown(); + } + } + + public void fillBrokerAddrIfNotExist(DefaultMQPullConsumer defaultMQPullConsumer, MessageQueue messageQueue, + String routeTopic) { + + FindBrokerResult findBrokerResult = defaultMQPullConsumer.getDefaultMQPullConsumerImpl().getRebalanceImpl().getmQClientFactory() + .findBrokerAddressInSubscribe(messageQueue.getBrokerName(), 0, false); + if (findBrokerResult == null) { + // use lmq parent topic to fill up broker addr table + defaultMQPullConsumer.getDefaultMQPullConsumerImpl().getRebalanceImpl().getmQClientFactory() + .updateTopicRouteInfoFromNameServer(routeTopic); + } + + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/message/QueryMsgByIdSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/message/QueryMsgByIdSubCommand.java new file mode 100644 index 0000000..e83029e --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/message/QueryMsgByIdSubCommand.java @@ -0,0 +1,326 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tools.command.message; + +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.List; +import org.apache.commons.cli.CommandLine; +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.client.producer.SendResult; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageClientExt; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.admin.api.MessageTrack; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class QueryMsgByIdSubCommand implements SubCommand { + public static void queryById(final DefaultMQAdminExt admin, final String clusterName, final String topic, + final String msgId, final Charset msgBodyCharset) throws MQClientException, + RemotingException, MQBrokerException, InterruptedException, IOException { + MessageExt msg = admin.queryMessage(clusterName, topic, msgId); + + printMsg(admin, msg, msgBodyCharset); + } + + public static void printMsg(final DefaultMQAdminExt admin, final MessageExt msg) throws IOException { + printMsg(admin, msg, null); + } + + public static void printMsg(final DefaultMQAdminExt admin, final MessageExt msg, + final Charset msgBodyCharset) throws IOException { + if (msg == null) { + System.out.printf("%nMessage not found!"); + return; + } + + String bodyTmpFilePath = createBodyFile(msg); + String msgId = msg.getMsgId(); + if (msg instanceof MessageClientExt) { + msgId = ((MessageClientExt) msg).getOffsetMsgId(); + } + + System.out.printf("%-20s %s%n", + "OffsetID:", + msgId + ); + + System.out.printf("%-20s %s%n", + "Topic:", + msg.getTopic() + ); + + System.out.printf("%-20s %s%n", + "Tags:", + "[" + msg.getTags() + "]" + ); + + System.out.printf("%-20s %s%n", + "Keys:", + "[" + msg.getKeys() + "]" + ); + + System.out.printf("%-20s %d%n", + "Queue ID:", + msg.getQueueId() + ); + + System.out.printf("%-20s %d%n", + "Queue Offset:", + msg.getQueueOffset() + ); + + System.out.printf("%-20s %d%n", + "CommitLog Offset:", + msg.getCommitLogOffset() + ); + + System.out.printf("%-20s %d%n", + "Reconsume Times:", + msg.getReconsumeTimes() + ); + + System.out.printf("%-20s %s%n", + "Born Timestamp:", + UtilAll.timeMillisToHumanString2(msg.getBornTimestamp()) + ); + + System.out.printf("%-20s %s%n", + "Store Timestamp:", + UtilAll.timeMillisToHumanString2(msg.getStoreTimestamp()) + ); + + System.out.printf("%-20s %s%n", + "Born Host:", + RemotingHelper.parseSocketAddressAddr(msg.getBornHost()) + ); + + System.out.printf("%-20s %s%n", + "Store Host:", + RemotingHelper.parseSocketAddressAddr(msg.getStoreHost()) + ); + + System.out.printf("%-20s %d%n", + "System Flag:", + msg.getSysFlag() + ); + + System.out.printf("%-20s %s%n", + "Properties:", + msg.getProperties() != null ? msg.getProperties().toString() : "" + ); + + System.out.printf("%-20s %s%n", + "Message Body Path:", + bodyTmpFilePath + ); + + if (null != msgBodyCharset) { + System.out.printf("%-20s %s%n", "Message Body:", new String(msg.getBody(), msgBodyCharset)); + } + + try { + List mtdList = admin.messageTrackDetail(msg); + if (mtdList.isEmpty()) { + System.out.printf("%n%nWARN: No Consumer"); + } else { + System.out.printf("%n%n"); + for (MessageTrack mt : mtdList) { + System.out.printf("%s%n", mt); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static String createBodyFile(MessageExt msg) throws IOException { + DataOutputStream dos = null; + try { + String bodyTmpFilePath = "/tmp/rocketmq/msgbodys"; + File file = new File(bodyTmpFilePath); + if (!file.exists()) { + file.mkdirs(); + } + bodyTmpFilePath = bodyTmpFilePath + "/" + msg.getMsgId(); + dos = new DataOutputStream(new FileOutputStream(bodyTmpFilePath)); + dos.write(msg.getBody()); + return bodyTmpFilePath; + } finally { + if (dos != null) + dos.close(); + } + } + + @Override + public String commandName() { + return "queryMsgById"; + } + + @Override + public String commandDesc() { + return "Query Message by Id."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("t", "topic", true, "topic name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("i", "msgId", true, "Message Id"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("g", "consumerGroup", true, "consumer group name"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("d", "clientId", true, "The consumer's client id"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("s", "sendMessage", true, "resend message"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("u", "unitName", true, "unit name"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("f", "bodyFormat", true, "print message body by the specified format"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "cluster", true, "Cluster name or lmq parent topic, lmq is used to find the route."); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + DefaultMQProducer defaultMQProducer = new DefaultMQProducer("ReSendMsgById"); + defaultMQProducer.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + defaultMQAdminExt.start(); + + String topic = commandLine.getOptionValue('t').trim(); + + if (commandLine.hasOption('s')) { + if (commandLine.hasOption('u')) { + String unitName = commandLine.getOptionValue('u').trim(); + defaultMQProducer.setUnitName(unitName); + } + defaultMQProducer.start(); + } + + final String msgIds = commandLine.getOptionValue('i').trim(); + final String[] msgIdArr = StringUtils.split(msgIds, ","); + String clusterName = commandLine.hasOption('c') ? commandLine.getOptionValue('c').trim() : null; + + if (commandLine.hasOption('g') && commandLine.hasOption('d')) { + final String consumerGroup = commandLine.getOptionValue('g').trim(); + final String clientId = commandLine.getOptionValue('d').trim(); + for (String msgId : msgIdArr) { + if (StringUtils.isNotBlank(msgId)) { + pushMsg(defaultMQAdminExt, clusterName, consumerGroup, clientId, topic, msgId.trim()); + } + } + } else if (commandLine.hasOption('s')) { + boolean resend = Boolean.parseBoolean(commandLine.getOptionValue('s', "false").trim()); + if (resend) { + for (String msgId : msgIdArr) { + if (StringUtils.isNotBlank(msgId)) { + sendMsg(defaultMQAdminExt, clusterName, defaultMQProducer, topic, msgId.trim()); + } + } + } + } else { + Charset msgBodyCharset = null; + if (commandLine.hasOption('f')) { + msgBodyCharset = Charset.forName(commandLine.getOptionValue('f').trim()); + } + for (String msgId : msgIdArr) { + if (StringUtils.isNotBlank(msgId)) { + queryById(defaultMQAdminExt, clusterName, topic, msgId.trim(), msgBodyCharset); + } + } + + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQProducer.shutdown(); + defaultMQAdminExt.shutdown(); + } + } + + private void pushMsg(final DefaultMQAdminExt defaultMQAdminExt, final String clusterName, + final String consumerGroup, final String clientId, + final String topic, final String msgId) { + try { + ConsumerRunningInfo consumerRunningInfo = defaultMQAdminExt.getConsumerRunningInfo(consumerGroup, clientId, false, false); + if (consumerRunningInfo != null && ConsumerRunningInfo.isPushType(consumerRunningInfo)) { + ConsumeMessageDirectlyResult result = + defaultMQAdminExt.consumeMessageDirectly(clusterName, consumerGroup, clientId, topic, msgId); + System.out.printf("%s", result); + } else { + System.out.printf("this %s client is not push consumer ,not support direct push \n", clientId); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void sendMsg(final DefaultMQAdminExt defaultMQAdminExt, final String clusterName, + final DefaultMQProducer defaultMQProducer, + final String topic, final String msgId) { + try { + MessageExt msg = defaultMQAdminExt.queryMessage(clusterName, topic, msgId); + if (msg != null) { + // resend msg by id + System.out.printf("prepare resend msg. originalMsgId=%s", msgId); + SendResult result = defaultMQProducer.send(msg); + System.out.printf("%s", result); + } else { + System.out.printf("no message. msgId=%s", msgId); + } + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/message/QueryMsgByKeySubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/message/QueryMsgByKeySubCommand.java new file mode 100644 index 0000000..02961c3 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/message/QueryMsgByKeySubCommand.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.tools.command.message; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.client.QueryResult; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.exception.RemotingException; + +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class QueryMsgByKeySubCommand implements SubCommand { + + @Override + public String commandName() { + return "queryMsgByKey"; + } + + @Override + public String commandDesc() { + return "Query Message by Key."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("t", "topic", true, "Topic name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("k", "msgKey", true, "Message Key"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("b", "beginTimestamp", true, "Begin timestamp(ms). default:0, eg:1676730526212"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("e", "endTimestamp", true, "End timestamp(ms). default:Long.MAX_VALUE, eg:1676730526212"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("m", "maxNum", true, "The maximum number of messages returned by the query, default:64"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "cluster", true, "Cluster name or lmq parent topic, lmq is used to find the route."); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + final String topic = commandLine.getOptionValue('t').trim(); + final String key = commandLine.getOptionValue('k').trim(); + + long beginTimestamp = 0; + long endTimestamp = Long.MAX_VALUE; + int maxNum = 64; + String clusterName = null; + if (commandLine.hasOption("b")) { + beginTimestamp = Long.parseLong(commandLine.getOptionValue("b").trim()); + } + if (commandLine.hasOption("e")) { + endTimestamp = Long.parseLong(commandLine.getOptionValue("e").trim()); + } + if (commandLine.hasOption("m")) { + maxNum = Integer.parseInt(commandLine.getOptionValue("m").trim()); + } + if (commandLine.hasOption("c")) { + clusterName = commandLine.getOptionValue("c").trim(); + } + this.queryByKey(defaultMQAdminExt, clusterName, topic, key, maxNum, beginTimestamp, endTimestamp); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } + + private void queryByKey(final DefaultMQAdminExt admin, final String cluster, final String topic, final String key, int maxNum, long begin, + long end) + throws MQClientException, InterruptedException, RemotingException { + admin.start(); + + QueryResult queryResult = admin.queryMessage(cluster, topic, key, maxNum, begin, end); + + System.out.printf("%-50s %4s %40s%n", + "#Message ID", + "#QID", + "#Offset"); + for (MessageExt msg : queryResult.getMessageList()) { + System.out.printf("%-50s %4d %40d%n", msg.getMsgId(), msg.getQueueId(), msg.getQueueOffset()); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/message/QueryMsgByOffsetSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/message/QueryMsgByOffsetSubCommand.java new file mode 100644 index 0000000..14d0625 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/message/QueryMsgByOffsetSubCommand.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.tools.command.message; + +import java.nio.charset.Charset; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class QueryMsgByOffsetSubCommand implements SubCommand { + + @Override + public String commandName() { + return "queryMsgByOffset"; + } + + @Override + public String commandDesc() { + return "Query Message by offset."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("t", "topic", true, "topic name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("b", "brokerName", true, "Broker Name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("i", "queueId", true, "Queue Id"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("o", "offset", true, "Queue Offset"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("f", "bodyFormat", true, "print message body by the specified format"); + opt.setRequired(false); + options.addOption(opt); + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + DefaultMQPullConsumer defaultMQPullConsumer = new DefaultMQPullConsumer(MixAll.TOOLS_CONSUMER_GROUP, rpcHook); + + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + defaultMQPullConsumer.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + String topic = commandLine.getOptionValue('t').trim(); + String brokerName = commandLine.getOptionValue('b').trim(); + String queueId = commandLine.getOptionValue('i').trim(); + String offset = commandLine.getOptionValue('o').trim(); + Charset msgBodyCharset = null; + if (commandLine.hasOption('f')) { + msgBodyCharset = Charset.forName(commandLine.getOptionValue('f').trim()); + } + + MessageQueue mq = new MessageQueue(); + mq.setTopic(topic); + mq.setBrokerName(brokerName); + mq.setQueueId(Integer.parseInt(queueId)); + + defaultMQPullConsumer.start(); + defaultMQAdminExt.start(); + + PullResult pullResult = defaultMQPullConsumer.pull(mq, "*", Long.parseLong(offset), 1); + if (pullResult != null) { + switch (pullResult.getPullStatus()) { + case FOUND: + QueryMsgByIdSubCommand.printMsg(defaultMQAdminExt, pullResult.getMsgFoundList().get(0), msgBodyCharset); + break; + case NO_MATCHED_MSG: + case NO_NEW_MSG: + case OFFSET_ILLEGAL: + default: + break; + } + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQPullConsumer.shutdown(); + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/message/QueryMsgByUniqueKeySubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/message/QueryMsgByUniqueKeySubCommand.java new file mode 100644 index 0000000..5295d91 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/message/QueryMsgByUniqueKeySubCommand.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.tools.command.message; + +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.List; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.client.QueryResult; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.admin.api.MessageTrack; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class QueryMsgByUniqueKeySubCommand implements SubCommand { + + private DefaultMQAdminExt defaultMQAdminExt; + + private DefaultMQAdminExt createMQAdminExt(RPCHook rpcHook) throws SubCommandException { + if (this.defaultMQAdminExt != null) { + return defaultMQAdminExt; + } else { + defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + try { + defaultMQAdminExt.start(); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } + return defaultMQAdminExt; + } + } + + public static void queryById(final DefaultMQAdminExt admin, final String clusterName, final String topic, + final String msgId, + final boolean showAll) throws MQClientException, InterruptedException, IOException { + + QueryResult queryResult = admin.queryMessageByUniqKey(clusterName, topic, msgId, 32, 0, Long.MAX_VALUE); + assert queryResult != null; + List list = queryResult.getMessageList(); + if (list == null || list.size() == 0) { + return; + } + list.sort((o1, o2) -> (int) (o1.getStoreTimestamp() - o2.getStoreTimestamp())); + for (int i = 0; i < (showAll ? list.size() : 1); i++) { + showMessage(admin, list.get(i), i); + } + } + + private static void showMessage(final DefaultMQAdminExt admin, MessageExt msg, int index) throws IOException { + String bodyTmpFilePath = createBodyFile(msg, index); + + final String strFormat = "%-20s %s%n"; + final String intFormat = "%-20s %d%n"; + + System.out.printf(strFormat, "Topic:", msg.getTopic()); + System.out.printf(strFormat, "Tags:", "[" + msg.getTags() + "]"); + System.out.printf(strFormat, "Keys:", "[" + msg.getKeys() + "]"); + System.out.printf(intFormat, "Queue ID:", msg.getQueueId()); + System.out.printf(intFormat, "Queue Offset:", msg.getQueueOffset()); + System.out.printf(intFormat, "CommitLog Offset:", msg.getCommitLogOffset()); + System.out.printf(intFormat, "Reconsume Times:", msg.getReconsumeTimes()); + System.out.printf(strFormat, "Born Timestamp:", UtilAll.timeMillisToHumanString2(msg.getBornTimestamp())); + System.out.printf(strFormat, "Store Timestamp:", UtilAll.timeMillisToHumanString2(msg.getStoreTimestamp())); + System.out.printf(strFormat, "Born Host:", RemotingHelper.parseSocketAddressAddr(msg.getBornHost())); + System.out.printf(strFormat, "Store Host:", RemotingHelper.parseSocketAddressAddr(msg.getStoreHost())); + System.out.printf(intFormat, "System Flag:", msg.getSysFlag()); + System.out.printf(strFormat, "Properties:", + msg.getProperties() != null ? msg.getProperties().toString() : ""); + System.out.printf(strFormat, "Message Body Path:", bodyTmpFilePath); + + try { + List mtdList = admin.messageTrackDetail(msg); + if (mtdList.isEmpty()) { + System.out.printf("%n%nWARN: No Consumer"); + } else { + System.out.printf("%n%n"); + for (MessageTrack mt : mtdList) { + System.out.printf("%s", mt); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static String createBodyFile(MessageExt msg, int index) throws IOException { + DataOutputStream dos = null; + try { + StringBuilder bodyTmpFilePath = new StringBuilder("/tmp/rocketmq/msgbodys"); + File file = new File(bodyTmpFilePath.toString()); + if (!file.exists()) { + file.mkdirs(); + } + bodyTmpFilePath.append("/").append(msg.getMsgId()); + if (index > 0) { + bodyTmpFilePath.append("_" + index); + } + dos = new DataOutputStream(new FileOutputStream(bodyTmpFilePath.toString())); + dos.write(msg.getBody()); + return bodyTmpFilePath.toString(); + } finally { + if (dos != null) { + dos.close(); + } + } + } + + @Override + public String commandName() { + return "queryMsgByUniqueKey"; + } + + @Override + public String commandDesc() { + return "Query Message by Unique key."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("i", "msgId", true, "Message Id"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("g", "consumerGroup", true, "consumer group name"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("d", "clientId", true, "The consumer's client id"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("t", "topic", true, "The topic of msg"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("a", "showAll", false, "Print all message, the limit is 32"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "cluster", true, "Cluster name or lmq parent topic, lmq is used to find the route."); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + + try { + defaultMQAdminExt = createMQAdminExt(rpcHook); + + final String msgId = commandLine.getOptionValue('i').trim(); + final String topic = commandLine.getOptionValue('t').trim(); + String clusterName = commandLine.hasOption('c') ? commandLine.getOptionValue('c').trim() : null; + final boolean showAll = commandLine.hasOption('a'); + if (commandLine.hasOption('g') && commandLine.hasOption('d')) { + final String consumerGroup = commandLine.getOptionValue('g').trim(); + final String clientId = commandLine.getOptionValue('d').trim(); + ConsumerRunningInfo consumerRunningInfo = null; + try { + consumerRunningInfo = defaultMQAdminExt.getConsumerRunningInfo(consumerGroup, clientId, false, false); + } catch (Exception e) { + System.out.printf("get consumer runtime info for %s client failed \n", clientId); + } + if (consumerRunningInfo != null && ConsumerRunningInfo.isPushType(consumerRunningInfo)) { + ConsumeMessageDirectlyResult result = + defaultMQAdminExt.consumeMessageDirectly(consumerGroup, clientId, topic, msgId); + System.out.printf("%s", result); + } else { + System.out.printf("get consumer info failed or this %s client is not push consumer, not support direct push \n", clientId); + } + + } else { + queryById(defaultMQAdminExt, clusterName, topic, msgId, showAll); + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/message/QueryMsgTraceByIdSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/message/QueryMsgTraceByIdSubCommand.java new file mode 100644 index 0000000..2c546ec --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/message/QueryMsgTraceByIdSubCommand.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.tools.command.message; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.lang3.time.DateFormatUtils; +import org.apache.rocketmq.client.QueryResult; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.trace.TraceType; +import org.apache.rocketmq.client.trace.TraceView; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class QueryMsgTraceByIdSubCommand implements SubCommand { + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("i", "msgId", true, "Message Id"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("t", "traceTopic", true, "The name value of message trace topic"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("b", "beginTimestamp", true, "Begin timestamp(ms). default:0, eg:1676730526212"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("e", "endTimestamp", true, "End timestamp(ms). default:Long.MAX_VALUE, eg:1676730526212"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "maxNum", true, "The maximum number of messages returned by the query, default:64"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public String commandDesc() { + return "Query a message trace."; + } + + @Override + public String commandName() { + return "queryMsgTraceById"; + } + + @Override + public String commandAlias() { + return "QueryMsgTraceById"; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + try { + final String msgId = commandLine.getOptionValue('i').trim(); + String traceTopic = TopicValidator.RMQ_SYS_TRACE_TOPIC; + if (commandLine.hasOption('t')) { + traceTopic = commandLine.getOptionValue('t').trim(); + } + if (commandLine.hasOption('n')) { + defaultMQAdminExt.setNamesrvAddr(commandLine.getOptionValue('n').trim()); + } + + long beginTimestamp = 0; + long endTimestamp = Long.MAX_VALUE; + int maxNum = 64; + if (commandLine.hasOption("b")) { + beginTimestamp = Long.parseLong(commandLine.getOptionValue("b").trim()); + } + if (commandLine.hasOption("e")) { + endTimestamp = Long.parseLong(commandLine.getOptionValue("e").trim()); + } + if (commandLine.hasOption("c")) { + maxNum = Integer.parseInt(commandLine.getOptionValue("c").trim()); + } + + this.queryTraceByMsgId(defaultMQAdminExt, traceTopic, msgId, maxNum, beginTimestamp, endTimestamp); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + "command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } + + private void queryTraceByMsgId(final DefaultMQAdminExt admin, String traceTopic, String msgId, int maxNum, + long begin, long end) + throws MQClientException, InterruptedException { + admin.start(); + QueryResult queryResult = admin.queryMessage(traceTopic, msgId, maxNum, begin, end); + List messageList = queryResult.getMessageList(); + List traceViews = new ArrayList<>(); + for (MessageExt message : messageList) { + List traceView = TraceView.decodeFromTraceTransData(msgId, message); + traceViews.addAll(traceView); + } + + this.printMessageTrace(traceViews); + } + + private void printMessageTrace(List traceViews) { + Map> consumerTraceMap = new HashMap<>(16); + for (TraceView traceView : traceViews) { + if (traceView.getMsgType().equals(TraceType.Pub.name())) { + System.out.printf("%-10s %-20s %-20s %-20s %-10s %-10s%n", + "#Type", + "#ProducerGroup", + "#ClientHost", + "#SendTime", + "#CostTimes", + "#Status" + ); + System.out.printf("%-10s %-20s %-20s %-20s %-10s %-10s%n", + "Pub", + traceView.getGroupName(), + traceView.getClientHost(), + DateFormatUtils.format(traceView.getTimeStamp(), "yyyy-MM-dd HH:mm:ss"), + traceView.getCostTime() + "ms", + traceView.getStatus() + ); + System.out.printf("\n"); + } + if (traceView.getMsgType().equals(TraceType.SubAfter.name())) { + String groupName = traceView.getGroupName(); + if (consumerTraceMap.containsKey(groupName)) { + consumerTraceMap.get(groupName).add(traceView); + } else { + ArrayList views = new ArrayList<>(); + views.add(traceView); + consumerTraceMap.put(groupName, views); + } + } + } + + Iterator consumers = consumerTraceMap.keySet().iterator(); + while (consumers.hasNext()) { + System.out.printf("%-10s %-20s %-20s %-20s %-10s %-10s%n", + "#Type", + "#ConsumerGroup", + "#ClientHost", + "#ConsumerTime", + "#CostTimes", + "#Status" + ); + List consumerTraces = consumerTraceMap.get(consumers.next()); + for (TraceView traceView : consumerTraces) { + System.out.printf("%-10s %-20s %-20s %-20s %-10s %-10s%n", + "Sub", + traceView.getGroupName(), + traceView.getClientHost(), + DateFormatUtils.format(traceView.getTimeStamp(), "yyyy-MM-dd HH:mm:ss"), + traceView.getCostTime() + "ms", + traceView.getStatus() + ); + } + System.out.printf("\n"); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/message/SendMessageCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/message/SendMessageCommand.java new file mode 100644 index 0000000..970da6b --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/message/SendMessageCommand.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.tools.command.message; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +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.MessageQueue; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +import java.nio.charset.StandardCharsets; + +public class SendMessageCommand implements SubCommand { + + private DefaultMQProducer producer; + + @Override + public String commandName() { + return "sendMessage"; + } + + @Override + public String commandDesc() { + return "Send a message."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("t", "topic", true, "Topic name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("p", "body", true, "UTF-8 string format of the message body"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("k", "key", true, "Message keys"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "tags", true, "Message tags"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("b", "broker", true, "Send message to target broker"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("i", "qid", true, "Send message to target queue"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("m", "msgTraceEnable", true, "Message Trace Enable, Default: false"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + private DefaultMQProducer createProducer(RPCHook rpcHook, boolean msgTraceEnable) { + if (this.producer != null) { + return producer; + } else { + producer = new DefaultMQProducer(null, rpcHook, msgTraceEnable, null); + producer.setProducerGroup(Long.toString(System.currentTimeMillis())); + return producer; + } + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + Message msg = null; + String topic = commandLine.getOptionValue('t').trim(); + String body = commandLine.getOptionValue('p').trim(); + String tag = null; + String keys = null; + String brokerName = null; + int queueId = -1; + try { + if (commandLine.hasOption('k')) { + keys = commandLine.getOptionValue('k').trim(); + } + if (commandLine.hasOption('c')) { + tag = commandLine.getOptionValue('c').trim(); + } + if (commandLine.hasOption('b')) { + brokerName = commandLine.getOptionValue('b').trim(); + } + if (commandLine.hasOption('i')) { + if (!commandLine.hasOption('b')) { + System.out.print("Broker name must be set if the queue is chosen!"); + return; + } else { + queueId = Integer.parseInt(commandLine.getOptionValue('i').trim()); + } + } + msg = new Message(topic, tag, keys, body.getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + throw new RuntimeException(this.getClass().getSimpleName() + " command failed", e); + } + boolean msgTraceEnable = false; + if (commandLine.hasOption('m')) { + msgTraceEnable = Boolean.parseBoolean(commandLine.getOptionValue('m').trim()); + } + DefaultMQProducer producer = this.createProducer(rpcHook, msgTraceEnable); + SendResult result; + try { + producer.start(); + if (brokerName != null && queueId > -1) { + MessageQueue messageQueue = new MessageQueue(topic, brokerName, queueId); + result = producer.send(msg, messageQueue); + } else { + result = producer.send(msg); + } + + } catch (Exception e) { + throw new RuntimeException(this.getClass().getSimpleName() + " command failed", e); + } finally { + producer.shutdown(); + } + + System.out.printf("%-32s %-4s %-20s %s%n", + "#Broker Name", + "#QID", + "#Send Result", + "#MsgId" + ); + + if (result != null) { + System.out.printf("%-32s %-4s %-20s %s%n", + result.getMessageQueue().getBrokerName(), + result.getMessageQueue().getQueueId(), + result.getSendStatus(), + result.getMsgId() + ); + } else { + System.out.printf("%-32s %-4s %-20s %s%n", + "Unknown", + "Unknown", + "Failed", + "None" + ); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/metadata/RocksDBConfigToJsonCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/metadata/RocksDBConfigToJsonCommand.java new file mode 100644 index 0000000..48bc163 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/metadata/RocksDBConfigToJsonCommand.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.tools.command.metadata; + +import com.alibaba.fastjson.JSONObject; +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.config.ConfigRocksDBStorage; +import org.apache.rocketmq.common.utils.DataConverter; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.header.ExportRocksDBConfigToJsonRequestHeader; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.rocksdb.RocksIterator; + +public class RocksDBConfigToJsonCommand implements SubCommand { + + @Override + public String commandName() { + return "rocksDBConfigToJson"; + } + + @Override + public String commandDesc() { + return "Convert RocksDB kv config (topics/subscriptionGroups/consumerOffsets) to json. " + + "[rpc mode] Use [-n, -c, -b, -t] to send Request to broker ( version >= 5.3.2 ) or [local mode] use [-p, -t, -j, -e] to load RocksDB. " + + "If -e is provided, tools will export json file instead of std print"; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option configTypeOption = new Option("t", "configType", true, "Name of kv config, e.g. " + + "topics/subscriptionGroups/consumerOffsets. Required in local mode and default all in rpc mode."); + options.addOption(configTypeOption); + + // [local mode] options + Option pathOption = new Option("p", "configPath", true, + "[local mode] Absolute path to the metadata config directory"); + options.addOption(pathOption); + + Option exportPathOption = new Option("e", "exportFile", true, + "[local mode] Absolute file path for exporting, auto backup existing file, not directory. If exportFile is provided, will export Json file and ignore [-j]."); + options.addOption(exportPathOption); + + Option jsonEnableOption = new Option("j", "jsonEnable", true, + "[local mode] Json format enable, Default: true. If exportFile is provided, will export Json file and ignore [-j]."); + options.addOption(jsonEnableOption); + + // [rpc mode] options + Option nameserverOption = new Option("n", "nameserverAddr", true, + "[rpc mode] nameserverAddr. If nameserverAddr and clusterName are provided, will ignore [-p, -e, -j, -b] args"); + options.addOption(nameserverOption); + + Option clusterOption = new Option("c", "cluster", true, + "[rpc mode] Cluster name. If nameserverAddr and clusterName are provided, will ignore [-p, -e, -j, -b] args"); + options.addOption(clusterOption); + + Option brokerAddrOption = new Option("b", "brokerAddr", true, + "[rpc mode] Broker address. If brokerAddr is provided, will ignore [-p, -e, -j] args"); + options.addOption(brokerAddrOption); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + List typeList = getConfigTypeList(commandLine); + + if (commandLine.hasOption("nameserverAddr")) { + // [rpc mode] call all brokers in cluster to export to json file + System.out.print("Use [rpc mode] call all brokers in cluster to export to json file \n"); + checkRequiredArgsProvided(commandLine, "rpc mode", "cluster"); + handleRpcMode(commandLine, rpcHook, typeList); + } else if (commandLine.hasOption("brokerAddr")) { + // [rpc mode] call broker to export to json file + System.out.print("Use [rpc mode] call broker to export to json file \n"); + handleRpcMode(commandLine, rpcHook, typeList); + } else if (commandLine.hasOption("configPath")) { + // [local mode] load rocksdb to print or export file + System.out.print("Use [local mode] load rocksdb to print or export file \n"); + checkRequiredArgsProvided(commandLine, "local mode", "configType"); + handleLocalMode(commandLine); + } else { + System.out.print(commandDesc() + "\n"); + } + } + + private void handleLocalMode(CommandLine commandLine) { + ExportRocksDBConfigToJsonRequestHeader.ConfigType type = Objects.requireNonNull(getConfigTypeList(commandLine)).get(0); + String path = commandLine.getOptionValue("configPath").trim(); + if (StringUtils.isEmpty(path) || !new File(path).exists()) { + System.out.print("Rocksdb path is invalid.\n"); + return; + } + path = Paths.get(path, type.toString()).toString(); + String exportFile = commandLine.hasOption("exportFile") ? commandLine.getOptionValue("exportFile").trim() : null; + Map configMap = getConfigMapFromRocksDB(path, type); + if (configMap != null) { + if (exportFile == null) { + if (commandLine.hasOption("jsonEnable") && "false".equalsIgnoreCase(commandLine.getOptionValue("jsonEnable").trim())) { + printConfigMapJsonDisable(configMap); + } else { + System.out.print(JSONObject.toJSONString(configMap, true) + "\n"); + } + } else { + String jsonString = JSONObject.toJSONString(configMap, true); + try { + MixAll.string2File(jsonString, exportFile); + } catch (IOException e) { + System.out.print("persist file " + exportFile + " exception" + e); + } + } + } + } + + private void checkRequiredArgsProvided(CommandLine commandLine, String mode, + String... args) throws SubCommandException { + for (String arg : args) { + if (!commandLine.hasOption(arg)) { + System.out.printf("%s Invalid args, please input %s\n", mode, String.join(",", args)); + throw new SubCommandException("Invalid args"); + } + } + } + + private List getConfigTypeList(CommandLine commandLine) { + List typeList = new ArrayList<>(); + if (commandLine.hasOption("configType")) { + String configType = commandLine.getOptionValue("configType").trim(); + try { + typeList.addAll(ExportRocksDBConfigToJsonRequestHeader.ConfigType.fromString(configType)); + } catch (IllegalArgumentException e) { + System.out.print("Invalid configType: " + configType + " please input topics/subscriptionGroups/consumerOffsets \n"); + return null; + } + } else { + typeList.addAll(Arrays.asList(ExportRocksDBConfigToJsonRequestHeader.ConfigType.values())); + } + return typeList; + } + + private static void printConfigMapJsonDisable(Map configMap) { + AtomicLong count = new AtomicLong(0); + for (Map.Entry entry : configMap.entrySet()) { + String configKey = entry.getKey(); + System.out.printf("type: %s", configKey); + JSONObject jsonObject = entry.getValue(); + jsonObject.forEach((k, v) -> System.out.printf("%d, Key: %s, Value: %s%n", count.incrementAndGet(), k, v)); + } + } + + private static Map getConfigMapFromRocksDB(String path, + ExportRocksDBConfigToJsonRequestHeader.ConfigType configType) { + + if (ExportRocksDBConfigToJsonRequestHeader.ConfigType.CONSUMER_OFFSETS.equals(configType)) { + return loadConsumerOffsets(path); + } + + ConfigRocksDBStorage configRocksDBStorage = new ConfigRocksDBStorage(path, true); + configRocksDBStorage.start(); + RocksIterator iterator = configRocksDBStorage.iterator(); + try { + final Map configMap = new HashMap<>(); + final JSONObject configTable = new JSONObject(); + iterator.seekToFirst(); + while (iterator.isValid()) { + final byte[] key = iterator.key(); + final byte[] value = iterator.value(); + final String name = new String(key, DataConverter.CHARSET_UTF8); + final String config = new String(value, DataConverter.CHARSET_UTF8); + final JSONObject jsonObject = JSONObject.parseObject(config); + configTable.put(name, jsonObject); + iterator.next(); + } + byte[] kvDataVersion = configRocksDBStorage.getKvDataVersion(); + if (kvDataVersion != null) { + configMap.put("dataVersion", + JSONObject.parseObject(new String(kvDataVersion, DataConverter.CHARSET_UTF8))); + } + + if (ExportRocksDBConfigToJsonRequestHeader.ConfigType.TOPICS.equals(configType)) { + configMap.put("topicConfigTable", configTable); + } + if (ExportRocksDBConfigToJsonRequestHeader.ConfigType.SUBSCRIPTION_GROUPS.equals(configType)) { + configMap.put("subscriptionGroupTable", configTable); + } + return configMap; + } catch (Exception e) { + System.out.print("Error occurred while converting RocksDB kv config to json, " + "configType=" + configType + ", " + e.getMessage() + "\n"); + } finally { + configRocksDBStorage.shutdown(); + } + return null; + } + + private void handleRpcMode(CommandLine commandLine, RPCHook rpcHook, + List type) { + String nameserverAddr = commandLine.hasOption('n') ? commandLine.getOptionValue("nameserverAddr").trim() : null; + String inputBrokerAddr = commandLine.hasOption('b') ? commandLine.getOptionValue('b').trim() : null; + String clusterName = commandLine.hasOption('c') ? commandLine.getOptionValue('c').trim() : null; + + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook, 30 * 1000); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + defaultMQAdminExt.setNamesrvAddr(nameserverAddr); + + List> futureList = new ArrayList<>(); + + try { + defaultMQAdminExt.start(); + if (clusterName != null) { + ClusterInfo clusterInfo = defaultMQAdminExt.examineBrokerClusterInfo(); + Map> clusterAddrTable = clusterInfo.getClusterAddrTable(); + Map brokerAddrTable = clusterInfo.getBrokerAddrTable(); + if (clusterAddrTable.get(clusterName) == null) { + System.out.print("clusterAddrTable is empty"); + return; + } + for (Map.Entry entry : brokerAddrTable.entrySet()) { + String brokerName = entry.getKey(); + BrokerData brokerData = entry.getValue(); + String brokerAddr = brokerData.getBrokerAddrs().get(0L); + futureList.add(sendRequest(type, defaultMQAdminExt, brokerAddr, brokerName)); + } + } else if (inputBrokerAddr != null) { + futureList.add(sendRequest(type, defaultMQAdminExt, inputBrokerAddr, null)); + } + CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).whenComplete( + (v, t) -> System.out.print("broker export done.") + ).join(); + } catch (Exception e) { + throw new RuntimeException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } + + private CompletableFuture sendRequest(List type, + DefaultMQAdminExt defaultMQAdminExt, String brokerAddr, String brokerName) { + return CompletableFuture.supplyAsync(() -> { + try { + defaultMQAdminExt.exportRocksDBConfigToJson(brokerAddr, type); + } catch (Throwable t) { + System.out.print((brokerName != null) ? brokerName : brokerAddr + " export error"); + throw new CompletionException(this.getClass().getSimpleName() + " command failed", t); + } + return null; + }); + } + + private static Map loadConsumerOffsets(String path) { + ConfigRocksDBStorage configRocksDBStorage = new ConfigRocksDBStorage(path, true); + configRocksDBStorage.start(); + RocksIterator iterator = configRocksDBStorage.iterator(); + try { + final Map configMap = new HashMap<>(); + final JSONObject configTable = new JSONObject(); + iterator.seekToFirst(); + while (iterator.isValid()) { + final byte[] key = iterator.key(); + final byte[] value = iterator.value(); + final String name = new String(key, DataConverter.CHARSET_UTF8); + final String config = new String(value, DataConverter.CHARSET_UTF8); + final RocksDBOffsetSerializeWrapper jsonObject = JSONObject.parseObject(config, RocksDBOffsetSerializeWrapper.class); + configTable.put(name, jsonObject.getOffsetTable()); + iterator.next(); + } + configMap.put("offsetTable", configTable); + return configMap; + } catch (Exception e) { + System.out.print("Error occurred while converting RocksDB kv config to json, " + "configType=consumerOffsets, " + e.getMessage() + "\n"); + } finally { + configRocksDBStorage.shutdown(); + } + return null; + } + + static class RocksDBOffsetSerializeWrapper { + private ConcurrentMap offsetTable = new ConcurrentHashMap<>(16); + + public ConcurrentMap getOffsetTable() { + return offsetTable; + } + + public void setOffsetTable(ConcurrentMap offsetTable) { + this.offsetTable = offsetTable; + } + } +} \ No newline at end of file diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/namesrv/AddWritePermSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/namesrv/AddWritePermSubCommand.java new file mode 100644 index 0000000..0b0a075 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/namesrv/AddWritePermSubCommand.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.tools.command.namesrv; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +import java.util.List; + +public class AddWritePermSubCommand implements SubCommand { + @Override + public String commandName() { + return "addWritePerm"; + } + + @Override + public String commandDesc() { + return "Add write perm of broker in all name server you defined in the -n param."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("b", "brokerName", true, "broker name"); + opt.setRequired(true); + options.addOption(opt); + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + try { + defaultMQAdminExt.start(); + String brokerName = commandLine.getOptionValue('b').trim(); + List namesrvList = defaultMQAdminExt.getNameServerAddressList(); + if (namesrvList != null) { + for (String namesrvAddr : namesrvList) { + try { + int addTopicCount = defaultMQAdminExt.addWritePermOfBroker(namesrvAddr, brokerName); + System.out.printf("add write perm of broker[%s] in name server[%s] OK, %d%n", + brokerName, + namesrvAddr, + addTopicCount + ); + } catch (Exception e) { + System.out.printf("add write perm of broker[%s] in name server[%s] Failed%n", + brokerName, + namesrvAddr + ); + e.printStackTrace(); + } + } + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + "command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/namesrv/DeleteKvConfigCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/namesrv/DeleteKvConfigCommand.java new file mode 100644 index 0000000..0a0cc06 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/namesrv/DeleteKvConfigCommand.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.tools.command.namesrv; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class DeleteKvConfigCommand implements SubCommand { + @Override + public String commandName() { + return "deleteKvConfig"; + } + + @Override + public String commandDesc() { + return "Delete KV config."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("s", "namespace", true, "set the namespace"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("k", "key", true, "set the key name"); + opt.setRequired(true); + options.addOption(opt); + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + try { + // namespace + String namespace = commandLine.getOptionValue('s').trim(); + // key name + String key = commandLine.getOptionValue('k').trim(); + + defaultMQAdminExt.start(); + defaultMQAdminExt.deleteKvConfig(namespace, key); + System.out.printf("delete kv config from namespace success.%n"); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/namesrv/GetNamesrvConfigCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/namesrv/GetNamesrvConfigCommand.java new file mode 100644 index 0000000..25c8d08 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/namesrv/GetNamesrvConfigCommand.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.tools.command.namesrv; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class GetNamesrvConfigCommand implements SubCommand { + + @Override + public String commandName() { + return "getNamesrvConfig"; + } + + @Override + public String commandDesc() { + return "Get configs of name server."; + } + + @Override + public Options buildCommandlineOptions(final Options options) { + return options; + } + + @Override + public void execute(final CommandLine commandLine, final Options options, + final RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + try { + // servers + String servers = commandLine.getOptionValue('n'); + List serverList = null; + if (servers != null && servers.length() > 0) { + String[] serverArray = servers.trim().split(";"); + + if (serverArray.length > 0) { + serverList = Arrays.asList(serverArray); + } + } + + defaultMQAdminExt.start(); + + Map nameServerConfigs = defaultMQAdminExt.getNameServerConfig(serverList); + + for (Entry nameServerConfigEntry : nameServerConfigs.entrySet()) { + System.out.printf("============%s============\n", + nameServerConfigEntry.getKey()); + for (Entry entry : nameServerConfigEntry.getValue().entrySet()) { + System.out.printf("%-50s= %s\n", entry.getKey(), entry.getValue()); + } + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} \ No newline at end of file diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/namesrv/UpdateKvConfigCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/namesrv/UpdateKvConfigCommand.java new file mode 100644 index 0000000..7d3a809 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/namesrv/UpdateKvConfigCommand.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.tools.command.namesrv; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class UpdateKvConfigCommand implements SubCommand { + @Override + public String commandName() { + return "updateKvConfig"; + } + + @Override + public String commandDesc() { + return "Create or update KV config."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("s", "namespace", true, "set the namespace"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("k", "key", true, "set the key name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("v", "value", true, "set the key value"); + opt.setRequired(true); + options.addOption(opt); + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + try { + // namespace + String namespace = commandLine.getOptionValue('s').trim(); + // key name + String key = commandLine.getOptionValue('k').trim(); + // key name + String value = commandLine.getOptionValue('v').trim(); + + if (commandLine.hasOption('n')) { + defaultMQAdminExt.setNamesrvAddr(commandLine.getOptionValue('n').trim()); + } + + defaultMQAdminExt.start(); + defaultMQAdminExt.createAndUpdateKvConfig(namespace, key, value); + System.out.printf("create or update kv config to namespace success.%n"); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/namesrv/UpdateNamesrvConfigCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/namesrv/UpdateNamesrvConfigCommand.java new file mode 100644 index 0000000..1bb5b38 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/namesrv/UpdateNamesrvConfigCommand.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.tools.command.namesrv; + +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class UpdateNamesrvConfigCommand implements SubCommand { + @Override + public String commandName() { + return "updateNamesrvConfig"; + } + + @Override + public String commandDesc() { + return "Update configs of name server."; + } + + @Override + public Options buildCommandlineOptions(final Options options) { + Option opt = new Option("k", "key", true, "config key"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("v", "value", true, "config value"); + opt.setRequired(true); + options.addOption(opt); + + return options; + } + + @Override + public void execute(final CommandLine commandLine, final Options options, + final RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + try { + // key name + String key = commandLine.getOptionValue('k').trim(); + // key name + String value = commandLine.getOptionValue('v').trim(); + Properties properties = new Properties(); + properties.put(key, value); + + // servers + String servers = commandLine.getOptionValue('n'); + List serverList = null; + if (servers != null && servers.length() > 0) { + String[] serverArray = servers.trim().split(";"); + + if (serverArray.length > 0) { + serverList = Arrays.asList(serverArray); + } + } + + defaultMQAdminExt.start(); + + defaultMQAdminExt.updateNameServerConfig(properties, serverList); + + System.out.printf("update name server config success!%s\n%s : %s\n", + serverList == null ? "" : serverList, key, value); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} \ No newline at end of file diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/namesrv/WipeWritePermSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/namesrv/WipeWritePermSubCommand.java new file mode 100644 index 0000000..637dd52 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/namesrv/WipeWritePermSubCommand.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.tools.command.namesrv; + +import java.util.List; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class WipeWritePermSubCommand implements SubCommand { + + @Override + public String commandName() { + return "wipeWritePerm"; + } + + @Override + public String commandDesc() { + return "Wipe write perm of broker in all name server you defined in the -n param."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("b", "brokerName", true, "broker name"); + opt.setRequired(true); + options.addOption(opt); + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + defaultMQAdminExt.start(); + String brokerName = commandLine.getOptionValue('b').trim(); + List namesrvList = defaultMQAdminExt.getNameServerAddressList(); + if (namesrvList != null) { + for (String namesrvAddr : namesrvList) { + try { + int wipeTopicCount = defaultMQAdminExt.wipeWritePermOfBroker(namesrvAddr, brokerName); + System.out.printf("wipe write perm of broker[%s] in name server[%s] OK, %d%n", + brokerName, + namesrvAddr, + wipeTopicCount + ); + } catch (Exception e) { + System.out.printf("wipe write perm of broker[%s] in name server[%s] Failed%n", + brokerName, + namesrvAddr + ); + + e.printStackTrace(); + } + } + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/offset/CloneGroupOffsetCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/offset/CloneGroupOffsetCommand.java new file mode 100644 index 0000000..d3d4349 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/offset/CloneGroupOffsetCommand.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.tools.command.offset; + +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.admin.ConsumeStats; +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.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class CloneGroupOffsetCommand implements SubCommand { + @Override + public String commandName() { + return "cloneGroupOffset"; + } + + @Override + public String commandDesc() { + return "Clone offset from other group."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("s", "srcGroup", true, "set source consumer group"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("d", "destGroup", true, "set destination consumer group"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("t", "topic", true, "set the topic"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("o", "offline", true, "the group or the topic is offline"); + opt.setRequired(false); + options.addOption(opt); + + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + String srcGroup = commandLine.getOptionValue("s").trim(); + String destGroup = commandLine.getOptionValue("d").trim(); + String topic = commandLine.getOptionValue("t").trim(); + + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName("admin-" + Long.toString(System.currentTimeMillis())); + + try { + defaultMQAdminExt.start(); + ConsumeStats consumeStats = defaultMQAdminExt.examineConsumeStats(srcGroup); + Set mqs = consumeStats.getOffsetTable().keySet(); + if (!mqs.isEmpty()) { + TopicRouteData topicRoute = defaultMQAdminExt.examineTopicRouteInfo(topic); + for (MessageQueue mq : mqs) { + String addr = null; + for (BrokerData brokerData : topicRoute.getBrokerDatas()) { + if (brokerData.getBrokerName().equals(mq.getBrokerName())) { + addr = brokerData.selectBrokerAddr(); + break; + } + } + long offset = consumeStats.getOffsetTable().get(mq).getConsumerOffset(); + if (offset >= 0) { + defaultMQAdminExt.updateConsumeOffset(addr, destGroup, mq, offset); + } + } + } + System.out.printf("clone group offset success. srcGroup[%s], destGroup=[%s], topic[%s]", + srcGroup, destGroup, topic); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/offset/GetConsumerStatusCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/offset/GetConsumerStatusCommand.java new file mode 100644 index 0000000..b731af5 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/offset/GetConsumerStatusCommand.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.tools.command.offset; + +import java.util.Map; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class GetConsumerStatusCommand implements SubCommand { + @Override + public String commandName() { + return "getConsumerStatus"; + } + + @Override + public String commandDesc() { + return "get consumer status from client."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("g", "group", true, "set the consumer group"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("t", "topic", true, "set the topic"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("i", "originClientId", true, "set the consumer clientId"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + try { + String group = commandLine.getOptionValue("g").trim(); + String topic = commandLine.getOptionValue("t").trim(); + String originClientId = ""; + if (commandLine.hasOption("i")) { + originClientId = commandLine.getOptionValue("i").trim(); + } + + if (commandLine.hasOption('n')) { + defaultMQAdminExt.setNamesrvAddr(commandLine.getOptionValue('n').trim()); + } + + defaultMQAdminExt.start(); + + Map> consumerStatusTable = + defaultMQAdminExt.getConsumeStatus(topic, group, originClientId); + System.out.printf("get consumer status from client. group=%s, topic=%s, originClientId=%s%n", + group, topic, originClientId); + + System.out.printf("%-50s %-15s %-15s %-20s%n", + "#clientId", + "#brokerName", + "#queueId", + "#offset"); + + for (Map.Entry> entry : consumerStatusTable.entrySet()) { + String clientId = entry.getKey(); + Map mqTable = entry.getValue(); + for (Map.Entry entry1 : mqTable.entrySet()) { + MessageQueue mq = entry1.getKey(); + System.out.printf("%-50s %-15s %-15d %-20d%n", + UtilAll.frontStringAtLeast(clientId, 50), + mq.getBrokerName(), + mq.getQueueId(), + mqTable.get(mq)); + } + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/offset/ResetOffsetByTimeCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/offset/ResetOffsetByTimeCommand.java new file mode 100644 index 0000000..84a301b --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/offset/ResetOffsetByTimeCommand.java @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.tools.command.offset; + +import java.util.Map; +import java.util.Objects; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class ResetOffsetByTimeCommand implements SubCommand { + + @Override + public String commandName() { + return "resetOffsetByTime"; + } + + @Override + public String commandDesc() { + return "Reset consumer offset by timestamp(without client restart)."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("g", "group", true, "set the consumer group"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("t", "topic", true, "set the topic"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("s", "timestamp", true, "set the timestamp[now|currentTimeMillis|yyyy-MM-dd#HH:mm:ss:SSS]"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("f", "force", true, "set the force rollback by timestamp switch[true|false]. Deprecated."); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "cplus", false, "reset c++ client offset. Deprecated."); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("b", "broker", true, "broker addr"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("q", "queue", true, "queue id"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("o", "offset", true, "Expect queue offset, not support old version broker"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "cluster", true, "Cluster name or lmq parent topic, lmq is used to find the route."); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + try { + String group = commandLine.getOptionValue("g").trim(); + String topic = commandLine.getOptionValue("t").trim(); + String timeStampStr = commandLine.getOptionValue("s").trim(); + String clusterName = commandLine.hasOption('c') ? commandLine.getOptionValue('c').trim() : null; + long timestamp = "now".equals(timeStampStr) ? System.currentTimeMillis() : 0; + + try { + if (timestamp == 0) { + timestamp = Long.parseLong(timeStampStr); + } + } catch (NumberFormatException e) { + timestamp = Objects.requireNonNull( + UtilAll.parseDate(timeStampStr, UtilAll.YYYY_MM_DD_HH_MM_SS_SSS)).getTime(); + } + + boolean force = true; + if (commandLine.hasOption('f')) { + force = Boolean.parseBoolean(commandLine.getOptionValue("f").trim()); + } + + boolean isC = commandLine.hasOption('c'); + + String brokerAddr = null; + if (commandLine.hasOption('b')) { + brokerAddr = commandLine.getOptionValue("b"); + } + int queueId = -1; + if (commandLine.hasOption("q")) { + queueId = Integer.parseInt(commandLine.getOptionValue('q')); + } + + if (commandLine.hasOption('n')) { + defaultMQAdminExt.setNamesrvAddr(commandLine.getOptionValue('n').trim()); + } + + Long offset = null; + if (commandLine.hasOption('o')) { + offset = Long.parseLong(commandLine.getOptionValue('o')); + } + + defaultMQAdminExt.start(); + + if (brokerAddr != null && queueId >= 0) { + System.out.printf("start reset consumer offset by specified, " + + "group[%s], topic[%s], queueId[%s], broker[%s], timestamp(string)[%s], timestamp(long)[%s]%n", + group, topic, queueId, brokerAddr, timeStampStr, timestamp); + + long resetOffset = null != offset ? offset : + defaultMQAdminExt.searchOffset(brokerAddr, topic, queueId, timestamp, 3000); + + System.out.printf("reset consumer offset to %d%n", resetOffset); + if (resetOffset > 0) { + defaultMQAdminExt.resetOffsetByQueueId(brokerAddr, group, topic, queueId, resetOffset); + } + return; + } + + Map offsetTable; + try { + offsetTable = defaultMQAdminExt.resetOffsetByTimestamp(clusterName, topic, group, timestamp, force, isC); + } catch (MQClientException e) { + // if consumer not online, use old command to reset + if (ResponseCode.CONSUMER_NOT_ONLINE == e.getResponseCode()) { + ResetOffsetByTimeOldCommand.resetOffset(defaultMQAdminExt, clusterName, group, topic, timestamp, force, timeStampStr); + return; + } + throw e; + } + + System.out.printf("start reset consumer offset by specified, " + + "group[%s], topic[%s], force[%s], timestamp(string)[%s], timestamp(long)[%s]%n", + group, topic, force, timeStampStr, timestamp); + + System.out.printf("%-40s %-40s %-40s%n", "#brokerName", "#queueId", "#offset"); + + for (Map.Entry entry : offsetTable.entrySet()) { + System.out.printf("%-40s %-40d %-40d%n", + UtilAll.frontStringAtLeast(entry.getKey().getBrokerName(), 32), + entry.getKey().getQueueId(), + entry.getValue()); + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/offset/ResetOffsetByTimeOldCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/offset/ResetOffsetByTimeOldCommand.java new file mode 100644 index 0000000..c179c5c --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/offset/ResetOffsetByTimeOldCommand.java @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.tools.command.offset; + +import java.util.Date; +import java.util.List; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.admin.RollbackStats; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class ResetOffsetByTimeOldCommand implements SubCommand { + + public static void resetOffset(DefaultMQAdminExt defaultMQAdminExt, String clusterName, String consumerGroup, + String topic, + long timestamp, boolean force, String timeStampStr) + throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + + List rollbackStatsList = + defaultMQAdminExt.resetOffsetByTimestampOld(clusterName, consumerGroup, topic, timestamp, force); + + System.out.printf("reset consumer offset by specified " + + "consumerGroup[%s], topic[%s], force[%s], timestamp(string)[%s], timestamp(long)[%s]%n", + consumerGroup, topic, force, timeStampStr, timestamp); + + System.out.printf("%-20s %-20s %-20s %-20s %-20s %-20s%n", + "#brokerName", + "#queueId", + "#brokerOffset", + "#consumerOffset", + "#timestampOffset", + "#resetOffset" + ); + + for (RollbackStats rollbackStats : rollbackStatsList) { + System.out.printf("%-20s %-20d %-20d %-20d %-20d %-20d%n", + UtilAll.frontStringAtLeast(rollbackStats.getBrokerName(), 32), + rollbackStats.getQueueId(), + rollbackStats.getBrokerOffset(), + rollbackStats.getConsumerOffset(), + rollbackStats.getTimestampOffset(), + rollbackStats.getRollbackOffset() + ); + } + } + + @Override + public String commandName() { + return "resetOffsetByTimeOld"; + } + + @Override + public String commandDesc() { + return "Reset consumer offset by timestamp(execute this command required client restart)."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("g", "group", true, "set the consumer group"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("t", "topic", true, "set the topic"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("s", "timestamp", true, "set the timestamp[currentTimeMillis|yyyy-MM-dd#HH:mm:ss:SSS]"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("f", "force", true, "set the force rollback by timestamp switch[true|false]"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "cluster", true, "Cluster name or lmq parent topic, lmq is used to find the route."); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + try { + String consumerGroup = commandLine.getOptionValue("g").trim(); + String topic = commandLine.getOptionValue("t").trim(); + String timeStampStr = commandLine.getOptionValue("s").trim(); + String clusterName = commandLine.hasOption('c') ? commandLine.getOptionValue('c').trim() : null; + long timestamp = 0; + try { + timestamp = Long.parseLong(timeStampStr); + } catch (NumberFormatException e) { + + Date date = UtilAll.parseDate(timeStampStr, UtilAll.YYYY_MM_DD_HH_MM_SS_SSS); + if (date != null) { + timestamp = UtilAll.parseDate(timeStampStr, UtilAll.YYYY_MM_DD_HH_MM_SS_SSS).getTime(); + } else { + System.out.printf("specified timestamp invalid.%n"); + return; + } + } + + boolean force = true; + if (commandLine.hasOption('f')) { + force = Boolean.parseBoolean(commandLine.getOptionValue("f").trim()); + } + defaultMQAdminExt.start(); + resetOffset(defaultMQAdminExt, clusterName, consumerGroup, topic, timestamp, force, timeStampStr); + + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/offset/SkipAccumulationSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/offset/SkipAccumulationSubCommand.java new file mode 100644 index 0000000..8f2ac2e --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/offset/SkipAccumulationSubCommand.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.tools.command.offset; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.admin.RollbackStats; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class SkipAccumulationSubCommand implements SubCommand { + + @Override + public String commandName() { + return "skipAccumulatedMessage"; + } + + @Override + public String commandDesc() { + return "Skip all messages that are accumulated (not consumed) currently."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("g", "group", true, "set the consumer group"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("t", "topic", true, "set the topic"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("f", "force", true, "set the force rollback by timestamp switch[true|false]"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "cluster", true, "Cluster name or lmq parent topic, lmq is used to find the route."); + opt.setRequired(false); + options.addOption(opt); + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + long timestamp = -1; + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + try { + String group = commandLine.getOptionValue("g").trim(); + String topic = commandLine.getOptionValue("t").trim(); + String clusterName = commandLine.hasOption('c') ? commandLine.getOptionValue('c').trim() : null; + boolean force = true; + if (commandLine.hasOption('f')) { + force = Boolean.valueOf(commandLine.getOptionValue("f").trim()); + } + + defaultMQAdminExt.start(); + Map offsetTable; + try { + offsetTable = defaultMQAdminExt.resetOffsetByTimestamp(clusterName, topic, group, timestamp, force); + } catch (MQClientException e) { + if (ResponseCode.CONSUMER_NOT_ONLINE == e.getResponseCode()) { + List rollbackStatsList = defaultMQAdminExt.resetOffsetByTimestampOld(group, topic, timestamp, force); + System.out.printf("%-20s %-20s %-20s %-20s %-20s %-20s%n", + "#brokerName", + "#queueId", + "#brokerOffset", + "#consumerOffset", + "#timestampOffset", + "#rollbackOffset" + ); + + for (RollbackStats rollbackStats : rollbackStatsList) { + System.out.printf("%-20s %-20d %-20d %-20d %-20d %-20d%n", + UtilAll.frontStringAtLeast(rollbackStats.getBrokerName(), 32), + rollbackStats.getQueueId(), + rollbackStats.getBrokerOffset(), + rollbackStats.getConsumerOffset(), + rollbackStats.getTimestampOffset(), + rollbackStats.getRollbackOffset() + ); + } + return; + } + throw e; + } + + System.out.printf("%-40s %-40s %-40s%n", + "#brokerName", + "#queueId", + "#offset"); + + Iterator> iterator = offsetTable.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + System.out.printf("%-40s %-40d %-40d%n", + UtilAll.frontStringAtLeast(entry.getKey().getBrokerName(), 32), + entry.getKey().getQueueId(), + entry.getValue()); + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/producer/ProducerSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/producer/ProducerSubCommand.java new file mode 100644 index 0000000..48be75d --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/producer/ProducerSubCommand.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.tools.command.producer; + +import java.util.List; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.ProducerInfo; +import org.apache.rocketmq.remoting.protocol.body.ProducerTableInfo; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.MQAdminStartup; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class ProducerSubCommand implements SubCommand { + + public static void main(String[] args) { + System.setProperty(MixAll.NAMESRV_ADDR_PROPERTY, "127.0.0.1:9876"); + MQAdminStartup.main(new String[]{new ProducerSubCommand().commandName(), "-b", "127.0.0.1:10911"}); + } + + @Override + public String commandName() { + return "producer"; + } + + @Override + public String commandDesc() { + return "Query producer's instances, connection, status, etc."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("b", "broker", true, "broker address"); + opt.setRequired(true); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + defaultMQAdminExt.start(); + String brokerAddr = commandLine.getOptionValue('b').trim(); + ProducerTableInfo cc = defaultMQAdminExt.getAllProducerInfo(brokerAddr); + if (cc != null && cc.getData() != null && !cc.getData().isEmpty()) { + for (String group : cc.getData().keySet()) { + List list = cc.getData().get(group); + if (list == null || list.isEmpty()) { + System.out.printf("producer group (%s) instances are empty\n", group); + continue; + } + for (ProducerInfo producer : list) { + System.out.printf("producer group (%s) instance : %s\n", group, producer.toString()); + } + } + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/queue/CheckRocksdbCqWriteProgressCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/queue/CheckRocksdbCqWriteProgressCommand.java new file mode 100644 index 0000000..a0fc9fc --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/queue/CheckRocksdbCqWriteProgressCommand.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.tools.command.queue; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.CheckRocksdbCqWriteResult; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; + +public class CheckRocksdbCqWriteProgressCommand implements SubCommand { + + @Override + public String commandName() { + return "checkRocksdbCqWriteProgress"; + } + + @Override + public String commandDesc() { + return "check if rocksdb cq is same as file cq"; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("c", "cluster", true, "cluster name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("n", "nameserverAddr", true, "nameserverAddr"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("t", "topic", true, "topic name"); + options.addOption(opt); + + opt = new Option("cf", "checkFrom", true, "check from time"); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + defaultMQAdminExt.setNamesrvAddr(StringUtils.trim(commandLine.getOptionValue('n'))); + String clusterName = commandLine.hasOption('c') ? commandLine.getOptionValue('c').trim() : ""; + String topic = commandLine.hasOption('t') ? commandLine.getOptionValue('t').trim() : ""; + // The default check is 30 days + long checkStoreTime = commandLine.hasOption("cf") + ? Long.parseLong(commandLine.getOptionValue("cf").trim()) + : System.currentTimeMillis() - TimeUnit.DAYS.toMillis(30L); + + try { + defaultMQAdminExt.start(); + ClusterInfo clusterInfo = defaultMQAdminExt.examineBrokerClusterInfo(); + Map> clusterAddrTable = clusterInfo.getClusterAddrTable(); + Map brokerAddrTable = clusterInfo.getBrokerAddrTable(); + if (clusterAddrTable.get(clusterName) == null) { + System.out.print("clusterAddrTable is empty"); + return; + } + for (Map.Entry entry : brokerAddrTable.entrySet()) { + String brokerName = entry.getKey(); + BrokerData brokerData = entry.getValue(); + String brokerAddr = brokerData.getBrokerAddrs().get(0L); + CheckRocksdbCqWriteResult result = defaultMQAdminExt.checkRocksdbCqWriteProgress(brokerAddr, topic, checkStoreTime); + if (result.getCheckStatus() == CheckRocksdbCqWriteResult.CheckStatus.CHECK_ERROR.getValue()) { + System.out.print(brokerName + " check error, please check log... errInfo: " + result.getCheckResult()); + } else { + System.out.print(brokerName + " check doing, please wait and get the result from log... \n"); + } + } + } catch (Exception e) { + throw new RuntimeException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/queue/QueryConsumeQueueCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/queue/QueryConsumeQueueCommand.java new file mode 100644 index 0000000..4902b8a --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/queue/QueryConsumeQueueCommand.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.tools.command.queue; + +import com.alibaba.fastjson.JSON; +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.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.ConsumeQueueData; +import org.apache.rocketmq.remoting.protocol.body.QueryConsumeQueueResponseBody; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; + +public class QueryConsumeQueueCommand implements SubCommand { + + public static void main(String[] args) { + QueryConsumeQueueCommand cmd = new QueryConsumeQueueCommand(); + + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-t TopicTest", "-q 0", "-i 6447", "-b 100.81.165.119:10911"}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, cmd.buildCommandlineOptions(options), + new DefaultParser()); + cmd.execute(commandLine, options, null); + } + + @Override + public String commandName() { + return "queryCq"; + } + + @Override + public String commandDesc() { + return "Query cq command."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("t", "topic", true, "topic name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("q", "queue", true, "queue num, ie. 1"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("i", "index", true, "start queue index."); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("c", "count", true, "how many."); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("b", "broker", true, "broker addr."); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("g", "consumer", true, "consumer group."); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + defaultMQAdminExt.start(); + + String topic = commandLine.getOptionValue("t").trim(); + int queueId = Integer.parseInt(commandLine.getOptionValue("q").trim()); + long index = Long.parseLong(commandLine.getOptionValue("i").trim()); + int count = Integer.parseInt(commandLine.getOptionValue("c", "10").trim()); + String broker = null; + if (commandLine.hasOption("b")) { + broker = commandLine.getOptionValue("b").trim(); + } + String consumerGroup = null; + if (commandLine.hasOption("g")) { + consumerGroup = commandLine.getOptionValue("g").trim(); + } + + if (StringUtils.isEmpty(broker)) { + TopicRouteData topicRouteData = defaultMQAdminExt.examineTopicRouteInfo(topic); + + if (topicRouteData == null || topicRouteData.getBrokerDatas() == null + || topicRouteData.getBrokerDatas().isEmpty()) { + throw new Exception("No topic route data!"); + } + + broker = topicRouteData.getBrokerDatas().get(0).getBrokerAddrs().get(0L); + } + + QueryConsumeQueueResponseBody queryConsumeQueueResponseBody = defaultMQAdminExt.queryConsumeQueue( + broker, topic, queueId, index, count, consumerGroup + ); + + if (queryConsumeQueueResponseBody.getSubscriptionData() != null) { + System.out.printf("Subscription data: \n%s\n", JSON.toJSONString(queryConsumeQueueResponseBody.getSubscriptionData(), true)); + System.out.print("======================================\n"); + } + + if (queryConsumeQueueResponseBody.getFilterData() != null) { + System.out.printf("Filter data: \n%s\n", queryConsumeQueueResponseBody.getFilterData()); + System.out.print("======================================\n"); + } + + System.out.printf("Queue data: \nmax: %d, min: %d\n", queryConsumeQueueResponseBody.getMaxQueueIndex(), + queryConsumeQueueResponseBody.getMinQueueIndex()); + System.out.print("======================================\n"); + + if (queryConsumeQueueResponseBody.getQueueData() != null) { + + long i = index; + for (ConsumeQueueData queueData : queryConsumeQueueResponseBody.getQueueData()) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("idx: " + i + "\n"); + + stringBuilder.append(queueData.toString() + "\n"); + + stringBuilder.append("======================================\n"); + + System.out.print(stringBuilder.toString()); + i++; + } + + } + + } catch (Exception e) { + e.printStackTrace(); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/stats/StatsAllSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/stats/StatsAllSubCommand.java new file mode 100644 index 0000000..96097a9 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/stats/StatsAllSubCommand.java @@ -0,0 +1,207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.tools.command.stats; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.stats.Stats; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.admin.ConsumeStats; +import org.apache.rocketmq.remoting.protocol.body.BrokerStatsData; +import org.apache.rocketmq.remoting.protocol.body.GroupList; +import org.apache.rocketmq.remoting.protocol.body.TopicList; +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.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class StatsAllSubCommand implements SubCommand { + public static void printTopicDetail(final DefaultMQAdminExt admin, final String topic, final boolean activeTopic) + throws RemotingException, MQClientException, InterruptedException, MQBrokerException { + TopicRouteData topicRouteData = admin.examineTopicRouteInfo(topic); + + GroupList groupList = admin.queryTopicConsumeByWho(topic); + + double inTPS = 0; + + long inMsgCntToday = 0; + + for (BrokerData bd : topicRouteData.getBrokerDatas()) { + String masterAddr = bd.getBrokerAddrs().get(MixAll.MASTER_ID); + if (masterAddr != null) { + try { + BrokerStatsData bsd = admin.viewBrokerStatsData(masterAddr, Stats.TOPIC_PUT_NUMS, topic); + inTPS += bsd.getStatsMinute().getTps(); + inMsgCntToday += compute24HourSum(bsd); + } catch (Exception e) { + } + } + } + + if (groupList != null && !groupList.getGroupList().isEmpty()) { + + for (String group : groupList.getGroupList()) { + double outTPS = 0; + long outMsgCntToday = 0; + + for (BrokerData bd : topicRouteData.getBrokerDatas()) { + String masterAddr = bd.getBrokerAddrs().get(MixAll.MASTER_ID); + if (masterAddr != null) { + try { + String statsKey = String.format("%s@%s", topic, group); + BrokerStatsData bsd = admin.viewBrokerStatsData(masterAddr, Stats.GROUP_GET_NUMS, statsKey); + outTPS += bsd.getStatsMinute().getTps(); + outMsgCntToday += compute24HourSum(bsd); + } catch (Exception e) { + } + } + } + + long accumulate = 0; + try { + ConsumeStats consumeStats = admin.examineConsumeStats(group, topic); + if (consumeStats != null) { + accumulate = consumeStats.computeTotalDiff(); + if (accumulate < 0) { + accumulate = 0; + } + } + } catch (Exception e) { + } + + if (!activeTopic || inMsgCntToday > 0 || + outMsgCntToday > 0) { + + System.out.printf("%-64s %-64s %12d %11.2f %11.2f %14d %14d%n", + UtilAll.frontStringAtLeast(topic, 64), + UtilAll.frontStringAtLeast(group, 64), + accumulate, + inTPS, + outTPS, + inMsgCntToday, + outMsgCntToday + ); + } + } + } else { + if (!activeTopic || inMsgCntToday > 0) { + + System.out.printf("%-64s %-64s %12d %11.2f %11s %14d %14s%n", + UtilAll.frontStringAtLeast(topic, 64), + "", + 0, + inTPS, + "", + inMsgCntToday, + "NO_CONSUMER" + ); + } + } + } + + public static long compute24HourSum(BrokerStatsData bsd) { + if (bsd.getStatsDay().getSum() != 0) { + return bsd.getStatsDay().getSum(); + } + + if (bsd.getStatsHour().getSum() != 0) { + return bsd.getStatsHour().getSum(); + } + + if (bsd.getStatsMinute().getSum() != 0) { + return bsd.getStatsMinute().getSum(); + } + + return 0; + } + + @Override + public String commandName() { + return "statsAll"; + } + + @Override + public String commandDesc() { + return "Topic and Consumer tps stats."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("a", "activeTopic", false, "print active topic only"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("t", "topic", true, "print select topic only"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + defaultMQAdminExt.start(); + + TopicList topicList = defaultMQAdminExt.fetchAllTopicList(); + + System.out.printf("%-64s %-64s %12s %11s %11s %14s %14s%n", + "#Topic", + "#Consumer Group", + "#Accumulation", + "#InTPS", + "#OutTPS", + "#InMsg24Hour", + "#OutMsg24Hour" + ); + + boolean activeTopic = commandLine.hasOption('a'); + String selectTopic = commandLine.getOptionValue('t'); + + for (String topic : topicList.getTopicList()) { + if (topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX) || topic.startsWith(MixAll.DLQ_GROUP_TOPIC_PREFIX)) { + continue; + } + + if (selectTopic != null && !selectTopic.isEmpty() && !topic.equals(selectTopic)) { + continue; + } + + try { + printTopicDetail(defaultMQAdminExt, topic, activeTopic); + } catch (Exception e) { + } + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/topic/AllocateMQSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/AllocateMQSubCommand.java new file mode 100644 index 0000000..6a9b81e --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/AllocateMQSubCommand.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.tools.command.topic; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.client.consumer.rebalance.AllocateMessageQueueAveragely; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class AllocateMQSubCommand implements SubCommand { + @Override + public String commandName() { + return "allocateMQ"; + } + + @Override + public String commandDesc() { + return "Allocate MQ."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("t", "topic", true, "topic name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("i", "ipList", true, "ipList"); + opt.setRequired(true); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt adminExt = new DefaultMQAdminExt(rpcHook); + adminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + try { + adminExt.start(); + + String topic = commandLine.getOptionValue('t').trim(); + String ips = commandLine.getOptionValue('i').trim(); + final String[] split = ips.split(","); + final List ipList = new LinkedList<>(); + for (String ip : split) { + ipList.add(ip); + } + + final TopicRouteData topicRouteData = adminExt.examineTopicRouteInfo(topic); + final Set mqs = MQClientInstance.topicRouteData2TopicSubscribeInfo(topic, topicRouteData); + + final AllocateMessageQueueAveragely averagely = new AllocateMessageQueueAveragely(); + + RebalanceResult rr = new RebalanceResult(); + + for (String i : ipList) { + final List mqResult = averagely.allocate("aa", i, new ArrayList<>(mqs), ipList); + rr.getResult().put(i, mqResult); + } + + final String json = RemotingSerializable.toJson(rr, false); + System.out.printf("%s%n", json); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + adminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/topic/DeleteTopicSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/DeleteTopicSubCommand.java new file mode 100644 index 0000000..bdee33c --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/DeleteTopicSubCommand.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.tools.command.topic; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class DeleteTopicSubCommand implements SubCommand { + public static void deleteTopic(final DefaultMQAdminExt adminExt, + final String clusterName, + final String topic + ) throws InterruptedException, MQBrokerException, RemotingException, MQClientException { + + Set masterBrokerAddressSet = CommandUtil.fetchMasterAddrByClusterName(adminExt, clusterName); + adminExt.deleteTopicInBroker(masterBrokerAddressSet, topic); + System.out.printf("delete topic [%s] from cluster [%s] success.%n", topic, clusterName); + + Set nameServerSet = null; + if (adminExt.getNamesrvAddr() != null) { + String[] ns = adminExt.getNamesrvAddr().trim().split(";"); + nameServerSet = new HashSet(Arrays.asList(ns)); + } + + adminExt.deleteTopicInNameServer(nameServerSet, clusterName, topic); + System.out.printf("delete topic [%s] from NameServer success.%n", topic); + } + + @Override + public String commandName() { + return "deleteTopic"; + } + + @Override + public String commandDesc() { + return "Delete topic from broker and NameServer."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("t", "topic", true, "topic name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("c", "clusterName", true, "delete topic from which cluster"); + opt.setRequired(true); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt adminExt = new DefaultMQAdminExt(rpcHook); + adminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + try { + String topic = commandLine.getOptionValue('t').trim(); + + if (commandLine.hasOption('c')) { + String clusterName = commandLine.getOptionValue('c').trim(); + + adminExt.start(); + deleteTopic(adminExt, clusterName, topic); + return; + } + + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + adminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/topic/RebalanceResult.java b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/RebalanceResult.java new file mode 100644 index 0000000..730d520 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/RebalanceResult.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.tools.command.topic; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.rocketmq.common.message.MessageQueue; + +public class RebalanceResult { + private Map> result = new HashMap<>(); + + public Map> getResult() { + return result; + } + + public void setResult(final Map> result) { + this.result = result; + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/topic/RemappingStaticTopicSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/RemappingStaticTopicSubCommand.java new file mode 100644 index 0000000..2a08fdb --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/RemappingStaticTopicSubCommand.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.tools.command.topic; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionGroup; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicConfigAndQueueMapping; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingUtils; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicRemappingDetailWrapper; +import org.apache.rocketmq.remoting.rpc.ClientMetadata; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.admin.MQAdminUtils; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class RemappingStaticTopicSubCommand implements SubCommand { + + @Override + public String commandName() { + return "remappingStaticTopic"; + } + + @Override + public String commandDesc() { + return "Remapping static topic."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + OptionGroup optionGroup = new OptionGroup(); + + Option opt = null; + + opt = new Option("c", "clusters", true, "remapping static topic to clusters, comma separated"); + optionGroup.addOption(opt); + + opt = new Option("b", "brokers", true, "remapping static topic to brokers, comma separated"); + optionGroup.addOption(opt); + + optionGroup.setRequired(true); + options.addOptionGroup(optionGroup); + + opt = new Option("t", "topic", true, "topic name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("mf", "mapFile", true, "The mapping data file name "); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("fr", "forceReplace", true, "Force replace the old mapping"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + + public void executeFromFile(final CommandLine commandLine, final Options options, + RPCHook rpcHook) throws SubCommandException { + + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + defaultMQAdminExt.start(); + String topic = commandLine.getOptionValue('t').trim(); + + String mapFileName = commandLine.getOptionValue('f').trim(); + String mapData = MixAll.file2String(mapFileName); + TopicRemappingDetailWrapper wrapper = TopicRemappingDetailWrapper.decode(mapData.getBytes(StandardCharsets.UTF_8), + TopicRemappingDetailWrapper.class); + //double check the config + TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, wrapper.getBrokerConfigMap()); + TopicQueueMappingUtils.checkAndBuildMappingItems(new ArrayList<>(TopicQueueMappingUtils.getMappingDetailFromConfig(wrapper.getBrokerConfigMap().values())), false, true); + + + boolean force = false; + if (commandLine.hasOption("fr") && Boolean.parseBoolean(commandLine.getOptionValue("fr").trim())) { + force = true; + } + MQAdminUtils.remappingStaticTopic(topic, wrapper.getBrokerToMapIn(), wrapper.getBrokerToMapOut(), wrapper.getBrokerConfigMap(), 10000, force, defaultMQAdminExt); + return; + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } + + + @Override + public void execute(final CommandLine commandLine, final Options options, + RPCHook rpcHook) throws SubCommandException { + if (!commandLine.hasOption('t')) { + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + return; + } + + if (commandLine.hasOption("f")) { + executeFromFile(commandLine, options, rpcHook); + return; + } + + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + ClientMetadata clientMetadata = new ClientMetadata(); + Map brokerConfigMap; + Set targetBrokers = new HashSet<>(); + + try { + defaultMQAdminExt.start(); + if (!commandLine.hasOption("b") && !commandLine.hasOption('c')) { + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + return; + } + String topic = commandLine.getOptionValue('t').trim(); + + ClusterInfo clusterInfo = defaultMQAdminExt.examineBrokerClusterInfo(); + if (clusterInfo == null + || clusterInfo.getClusterAddrTable().isEmpty()) { + throw new RuntimeException("The Cluster info is empty"); + } + clientMetadata.refreshClusterInfo(clusterInfo); + { + if (commandLine.hasOption("b")) { + String brokerStrs = commandLine.getOptionValue("b").trim(); + for (String broker: brokerStrs.split(",")) { + targetBrokers.add(broker.trim()); + } + } else if (commandLine.hasOption("c")) { + String clusters = commandLine.getOptionValue('c').trim(); + for (String cluster : clusters.split(",")) { + cluster = cluster.trim(); + if (clusterInfo.getClusterAddrTable().get(cluster) != null) { + targetBrokers.addAll(clusterInfo.getClusterAddrTable().get(cluster)); + } + } + } + if (targetBrokers.isEmpty()) { + throw new RuntimeException("Find none brokers, do nothing"); + } + for (String broker : targetBrokers) { + String addr = clientMetadata.findMasterBrokerAddr(broker); + if (addr == null) { + throw new RuntimeException("Can't find addr for broker " + broker); + } + } + } + + brokerConfigMap = MQAdminUtils.examineTopicConfigAll(topic, defaultMQAdminExt); + if (brokerConfigMap.isEmpty()) { + throw new RuntimeException("No topic route to do the remapping"); + } + Map.Entry maxEpochAndNum = TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, brokerConfigMap); + { + TopicRemappingDetailWrapper oldWrapper = new TopicRemappingDetailWrapper(topic, TopicRemappingDetailWrapper.TYPE_CREATE_OR_UPDATE, maxEpochAndNum.getKey(), brokerConfigMap, new HashSet<>(), new HashSet<>()); + String oldMappingDataFile = TopicQueueMappingUtils.writeToTemp(oldWrapper, false); + System.out.printf("The old mapping data is written to file " + oldMappingDataFile + "\n"); + } + + TopicRemappingDetailWrapper newWrapper = TopicQueueMappingUtils.remappingStaticTopic(topic, brokerConfigMap, targetBrokers); + + { + String newMappingDataFile = TopicQueueMappingUtils.writeToTemp(newWrapper, true); + System.out.printf("The old mapping data is written to file " + newMappingDataFile + "\n"); + } + + MQAdminUtils.completeNoTargetBrokers(newWrapper.getBrokerConfigMap(), defaultMQAdminExt); + + MQAdminUtils.remappingStaticTopic(topic, newWrapper.getBrokerToMapIn(), newWrapper.getBrokerToMapOut(), newWrapper.getBrokerConfigMap(), 10000, false, defaultMQAdminExt); + + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/topic/TopicClusterSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/TopicClusterSubCommand.java new file mode 100644 index 0000000..098f34f --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/TopicClusterSubCommand.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.tools.command.topic; + +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class TopicClusterSubCommand implements SubCommand { + + @Override + public String commandName() { + return "topicClusterList"; + } + + @Override + public String commandDesc() { + return "Get cluster info for topic."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("t", "topic", true, "topic name"); + opt.setRequired(true); + options.addOption(opt); + return options; + } + + @Override + public void execute(final CommandLine commandLine, final Options options, + RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + String topic = commandLine.getOptionValue('t').trim(); + try { + defaultMQAdminExt.start(); + Set clusters = defaultMQAdminExt.getTopicClusterList(topic); + for (String value : clusters) { + System.out.printf("%s%n", value); + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/topic/TopicListSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/TopicListSubCommand.java new file mode 100644 index 0000000..d9a279f --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/TopicListSubCommand.java @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tools.command.topic; + +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.body.GroupList; +import org.apache.rocketmq.remoting.protocol.body.TopicList; +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.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class TopicListSubCommand implements SubCommand { + + @Override + public String commandName() { + return "topicList"; + } + + @Override + public String commandDesc() { + return "Fetch all topic list from name server."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("c", "clusterModel", false, "clusterModel"); + opt.setRequired(false); + options.addOption(opt); + return options; + } + + @Override + public void execute(final CommandLine commandLine, final Options options, + RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + defaultMQAdminExt.start(); + if (commandLine.hasOption('c')) { + ClusterInfo clusterInfo = defaultMQAdminExt.examineBrokerClusterInfo(); + + System.out.printf("%-20s %-48s %-48s%n", + "#Cluster Name", + "#Topic", + "#Consumer Group" + ); + + TopicList topicList = defaultMQAdminExt.fetchAllTopicList(); + for (String topic : topicList.getTopicList()) { + if (topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX) + || topic.startsWith(MixAll.DLQ_GROUP_TOPIC_PREFIX)) { + continue; + } + + String clusterName = ""; + GroupList groupList = new GroupList(); + + try { + clusterName = + this.findTopicBelongToWhichCluster(topic, clusterInfo, defaultMQAdminExt); + groupList = defaultMQAdminExt.queryTopicConsumeByWho(topic); + } catch (Exception e) { + } + + if (null == groupList || groupList.getGroupList().isEmpty()) { + groupList = new GroupList(); + groupList.getGroupList().add(""); + } + + for (String group : groupList.getGroupList()) { + System.out.printf("%-20s %-64s %-64s%n", + UtilAll.frontStringAtLeast(clusterName, 20), + UtilAll.frontStringAtLeast(topic, 64), + UtilAll.frontStringAtLeast(group, 64) + ); + } + } + } else { + TopicList topicList = defaultMQAdminExt.fetchAllTopicList(); + for (String topic : topicList.getTopicList()) { + System.out.printf("%s%n", topic); + } + } + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } + + private String findTopicBelongToWhichCluster(final String topic, final ClusterInfo clusterInfo, + final DefaultMQAdminExt defaultMQAdminExt) throws RemotingException, MQClientException, + InterruptedException { + TopicRouteData topicRouteData = defaultMQAdminExt.examineTopicRouteInfo(topic); + + BrokerData brokerData = topicRouteData.getBrokerDatas().get(0); + + String brokerName = brokerData.getBrokerName(); + + Iterator>> it = clusterInfo.getClusterAddrTable().entrySet().iterator(); + while (it.hasNext()) { + Entry> next = it.next(); + if (next.getValue().contains(brokerName)) { + return next.getKey(); + } + } + return null; + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/topic/TopicRouteSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/TopicRouteSubCommand.java new file mode 100644 index 0000000..70949d3 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/TopicRouteSubCommand.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.tools.command.topic; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.RPCHook; +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.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class TopicRouteSubCommand implements SubCommand { + + private static final String FORMAT = "%-45s %-32s %-50s %-10s %-11s %-5s%n"; + + @Override + public String commandName() { + return "topicRoute"; + } + + @Override + public String commandDesc() { + return "Examine topic route info."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("t", "topic", true, "topic name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("l", "list", false, "Use list format to print data"); + opt.setRequired(false); + options.addOption(opt); + return options; + } + + @Override + public void execute(final CommandLine commandLine, final Options options, + RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + defaultMQAdminExt.start(); + + String topic = commandLine.getOptionValue('t').trim(); + TopicRouteData topicRouteData = defaultMQAdminExt.examineTopicRouteInfo(topic); + printData(topicRouteData, commandLine.hasOption('l')); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } + + private void printData(TopicRouteData topicRouteData, boolean useListFormat) { + if (!useListFormat) { + System.out.printf("%s%n", topicRouteData.toJson(true)); + return; + } + + int totalReadQueue = 0, totalWriteQueue = 0; + List queueDataList = topicRouteData.getQueueDatas(); + Map map = new HashMap<>(); + for (QueueData queueData : queueDataList) { + map.put(queueData.getBrokerName(), queueData); + } + queueDataList.sort(Comparator.comparing(QueueData::getBrokerName)); + + List brokerDataList = topicRouteData.getBrokerDatas(); + brokerDataList.sort(Comparator.comparing(BrokerData::getBrokerName)); + + System.out.printf(FORMAT, "#ClusterName", "#BrokerName", "#BrokerAddrs", "#ReadQueue", "#WriteQueue", "#Perm"); + + for (BrokerData brokerData : brokerDataList) { + String brokerName = brokerData.getBrokerName(); + QueueData queueData = map.get(brokerName); + totalReadQueue += queueData.getReadQueueNums(); + totalWriteQueue += queueData.getWriteQueueNums(); + System.out.printf(FORMAT, brokerData.getCluster(), brokerName, brokerData.getBrokerAddrs(), + queueData.getReadQueueNums(), queueData.getWriteQueueNums(), queueData.getPerm()); + } + + for (int i = 0; i < 158; i++) { + System.out.print("-"); + } + System.out.printf("%n"); + System.out.printf(FORMAT, "Total:", map.keySet().size(), "", totalReadQueue, totalWriteQueue, ""); + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/topic/TopicStatusSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/TopicStatusSubCommand.java new file mode 100644 index 0000000..ff91399 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/TopicStatusSubCommand.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.tools.command.topic; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.admin.TopicOffset; +import org.apache.rocketmq.remoting.protocol.admin.TopicStatsTable; +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.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class TopicStatusSubCommand implements SubCommand { + + @Override + public String commandName() { + return "topicStatus"; + } + + @Override + public String commandDesc() { + return "Examine topic Status info."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("t", "topic", true, "topic name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("c", "cluster", true, "cluster name or lmq parent topic, lmq is used to find the route."); + opt.setRequired(false); + options.addOption(opt); + return options; + } + + @Override + public void execute(final CommandLine commandLine, final Options options, + RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + + try { + TopicStatsTable topicStatsTable = new TopicStatsTable(); + defaultMQAdminExt.start(); + String topic = commandLine.getOptionValue('t').trim(); + + if (commandLine.hasOption('c')) { + String cluster = commandLine.getOptionValue('c').trim(); + TopicRouteData topicRouteData = defaultMQAdminExt.examineTopicRouteInfo(cluster); + + for (BrokerData bd : topicRouteData.getBrokerDatas()) { + String addr = bd.selectBrokerAddr(); + if (addr != null) { + TopicStatsTable tst = defaultMQAdminExt.examineTopicStats(addr, topic); + topicStatsTable.getOffsetTable().putAll(tst.getOffsetTable()); + } + } + } else { + topicStatsTable = defaultMQAdminExt.examineTopicStats(topic); + } + + List mqList = new LinkedList<>(); + mqList.addAll(topicStatsTable.getOffsetTable().keySet()); + Collections.sort(mqList); + + System.out.printf("%-32s %-4s %-20s %-20s %s%n", + "#Broker Name", + "#QID", + "#Min Offset", + "#Max Offset", + "#Last Updated" + ); + + for (MessageQueue mq : mqList) { + TopicOffset topicOffset = topicStatsTable.getOffsetTable().get(mq); + + String humanTimestamp = ""; + if (topicOffset.getLastUpdateTimestamp() > 0) { + humanTimestamp = UtilAll.timeMillisToHumanString2(topicOffset.getLastUpdateTimestamp()); + } + + System.out.printf("%-32s %-4d %-20d %-20d %s%n", + UtilAll.frontStringAtLeast(mq.getBrokerName(), 32), + mq.getQueueId(), + topicOffset.getMinOffset(), + topicOffset.getMaxOffset(), + humanTimestamp + ); + } + System.out.printf("%n"); + System.out.printf("Topic Put TPS: %s%n", topicStatsTable.getTopicPutTps()); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/topic/UpdateOrderConfCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/UpdateOrderConfCommand.java new file mode 100644 index 0000000..3040d04 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/UpdateOrderConfCommand.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.tools.command.topic; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.namesrv.NamesrvUtil; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class UpdateOrderConfCommand implements SubCommand { + + @Override + public String commandName() { + return "updateOrderConf"; + } + + @Override + public String commandDesc() { + return "Create or update or delete order conf."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("t", "topic", true, "topic name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("v", "orderConf", true, "set order conf [eg. brokerName1:num;brokerName2:num]"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("m", "method", true, "option type [eg. put|get|delete"); + opt.setRequired(true); + options.addOption(opt); + + return options; + } + + @Override + public void execute(final CommandLine commandLine, final Options options, + RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + String topic = commandLine.getOptionValue('t').trim(); + String type = commandLine.getOptionValue('m').trim(); + + if ("get".equals(type)) { + + defaultMQAdminExt.start(); + String orderConf = + defaultMQAdminExt.getKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG, topic); + System.out.printf("get orderConf success. topic=[%s], orderConf=[%s] ", topic, orderConf); + + return; + } else if ("put".equals(type)) { + + defaultMQAdminExt.start(); + String orderConf = ""; + if (commandLine.hasOption('v')) { + orderConf = commandLine.getOptionValue('v').trim(); + } + if (UtilAll.isBlank(orderConf)) { + throw new Exception("please set orderConf with option -v."); + } + + defaultMQAdminExt.createOrUpdateOrderConf(topic, orderConf, true); + System.out.printf("update orderConf success. topic=[%s], orderConf=[%s]", topic, + orderConf.toString()); + return; + } else if ("delete".equals(type)) { + + defaultMQAdminExt.start(); + defaultMQAdminExt.deleteKvConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG, topic); + System.out.printf("delete orderConf success. topic=[%s]", topic); + + return; + } + + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/topic/UpdateStaticTopicSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/UpdateStaticTopicSubCommand.java new file mode 100644 index 0000000..3daeee8 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/UpdateStaticTopicSubCommand.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.tools.command.topic; + +import java.nio.charset.StandardCharsets; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionGroup; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicConfigAndQueueMapping; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingUtils; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicRemappingDetailWrapper; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.admin.MQAdminUtils; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class UpdateStaticTopicSubCommand implements SubCommand { + + @Override + public String commandName() { + return "updateStaticTopic"; + } + + @Override + public String commandDesc() { + return "Update or create static topic, which has fixed number of queues."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + OptionGroup optionGroup = new OptionGroup(); + + Option opt = null; + + opt = new Option("c", "clusters", true, "remapping static topic to clusters, comma separated"); + optionGroup.addOption(opt); + + opt = new Option("b", "brokers", true, "remapping static topic to brokers, comma separated"); + optionGroup.addOption(opt); + + optionGroup.setRequired(true); + options.addOptionGroup(optionGroup); + + opt = new Option("t", "topic", true, "topic name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("qn", "totalQueueNum", true, "total queue num"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("mf", "mapFile", true, "The mapping data file name"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("fr", "forceReplace", true, "Force replace the old mapping"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + + + public void executeFromFile(final CommandLine commandLine, final Options options, + RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + defaultMQAdminExt.start(); + String topic = commandLine.getOptionValue('t').trim(); + String mapFileName = commandLine.getOptionValue('f').trim(); + String mapData = MixAll.file2String(mapFileName); + TopicRemappingDetailWrapper wrapper = TopicRemappingDetailWrapper.decode(mapData.getBytes(StandardCharsets.UTF_8), + TopicRemappingDetailWrapper.class); + //double check the config + TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, wrapper.getBrokerConfigMap()); + boolean force = false; + if (commandLine.hasOption("fr") && Boolean.parseBoolean(commandLine.getOptionValue("fr").trim())) { + force = true; + } + TopicQueueMappingUtils.checkAndBuildMappingItems(new ArrayList<>(TopicQueueMappingUtils.getMappingDetailFromConfig(wrapper.getBrokerConfigMap().values())), false, true); + + MQAdminUtils.completeNoTargetBrokers(wrapper.getBrokerConfigMap(), defaultMQAdminExt); + MQAdminUtils.updateTopicConfigMappingAll(wrapper.getBrokerConfigMap(), defaultMQAdminExt, false); + return; + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } + + + + @Override + public void execute(final CommandLine commandLine, final Options options, + RPCHook rpcHook) throws SubCommandException { + if (!commandLine.hasOption('t')) { + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + return; + } + + if (commandLine.hasOption("f")) { + executeFromFile(commandLine, options, rpcHook); + return; + } + + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + Map brokerConfigMap = new HashMap<>(); + Set targetBrokers = new HashSet<>(); + + try { + defaultMQAdminExt.start(); + if (!commandLine.hasOption("b") && !commandLine.hasOption('c') + || !commandLine.hasOption("qn")) { + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + return; + } + String topic = commandLine.getOptionValue('t').trim(); + + ClusterInfo clusterInfo = defaultMQAdminExt.examineBrokerClusterInfo(); + if (clusterInfo == null + || clusterInfo.getClusterAddrTable().isEmpty()) { + throw new RuntimeException("The Cluster info is empty"); + } + { + if (commandLine.hasOption("b")) { + String brokerStrs = commandLine.getOptionValue("b").trim(); + for (String broker: brokerStrs.split(",")) { + targetBrokers.add(broker.trim()); + } + } else if (commandLine.hasOption("c")) { + String clusters = commandLine.getOptionValue('c').trim(); + for (String cluster : clusters.split(",")) { + cluster = cluster.trim(); + if (clusterInfo.getClusterAddrTable().get(cluster) != null) { + targetBrokers.addAll(clusterInfo.getClusterAddrTable().get(cluster)); + } + } + } + if (targetBrokers.isEmpty()) { + throw new RuntimeException("Find none brokers, do nothing"); + } + } + + //get the existed topic config and mapping + + brokerConfigMap = MQAdminUtils.examineTopicConfigAll(topic, defaultMQAdminExt); + int queueNum = Integer.parseInt(commandLine.getOptionValue("qn").trim()); + + Map.Entry maxEpochAndNum = new AbstractMap.SimpleImmutableEntry<>(System.currentTimeMillis(), queueNum); + if (!brokerConfigMap.isEmpty()) { + maxEpochAndNum = TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, brokerConfigMap); + } + + { + TopicRemappingDetailWrapper oldWrapper = new TopicRemappingDetailWrapper(topic, TopicRemappingDetailWrapper.TYPE_CREATE_OR_UPDATE, maxEpochAndNum.getKey(), brokerConfigMap, new HashSet<>(), new HashSet<>()); + String oldMappingDataFile = TopicQueueMappingUtils.writeToTemp(oldWrapper, false); + System.out.printf("The old mapping data is written to file " + oldMappingDataFile + "\n"); + } + //add the existed brokers to target brokers + targetBrokers.addAll(brokerConfigMap.keySet()); + + //calculate the new data + TopicRemappingDetailWrapper newWrapper = TopicQueueMappingUtils.createTopicConfigMapping(topic, queueNum, targetBrokers, brokerConfigMap); + + { + String newMappingDataFile = TopicQueueMappingUtils.writeToTemp(newWrapper, true); + System.out.printf("The new mapping data is written to file " + newMappingDataFile + "\n"); + } + + MQAdminUtils.completeNoTargetBrokers(brokerConfigMap, defaultMQAdminExt); + MQAdminUtils.updateTopicConfigMappingAll(brokerConfigMap, defaultMQAdminExt, false); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/topic/UpdateTopicListSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/UpdateTopicListSubCommand.java new file mode 100644 index 0000000..a246059 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/UpdateTopicListSubCommand.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.tools.command.topic; + +import com.alibaba.fastjson2.JSON; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionGroup; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class UpdateTopicListSubCommand implements SubCommand { + @Override + public String commandName() { + return "updateTopicList"; + } + + @Override + public String commandDesc() { + return "create or update topic in batch"; + } + + @Override + public Options buildCommandlineOptions(Options options) { + final OptionGroup optionGroup = new OptionGroup(); + Option opt = new Option("b", "brokerAddr", true, "create topic to which broker"); + optionGroup.addOption(opt); + opt = new Option("c", "clusterName", true, "create topic to which cluster"); + optionGroup.addOption(opt); + optionGroup.setRequired(true); + options.addOptionGroup(optionGroup); + + opt = new Option("f", "filename", true, "Path to a file with list of org.apache.rocketmq.common.TopicConfig in json format"); + opt.setRequired(true); + options.addOption(opt); + + return options; + } + + @Override + public void execute(CommandLine commandLine, Options options, + RPCHook rpcHook) throws SubCommandException { + final DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + final String fileName = commandLine.getOptionValue('f').trim(); + + + try { + final Path filePath = Paths.get(fileName); + if (!Files.exists(filePath)) { + System.out.printf("the file path %s does not exists%n", fileName); + return; + } + final byte[] topicConfigListBytes = Files.readAllBytes(filePath); + final List topicConfigs = JSON.parseArray(topicConfigListBytes, TopicConfig.class); + if (null == topicConfigs || topicConfigs.isEmpty()) { + return; + } + + if (commandLine.hasOption('b')) { + String brokerAddress = commandLine.getOptionValue('b').trim(); + defaultMQAdminExt.start(); + defaultMQAdminExt.createAndUpdateTopicConfigList(brokerAddress, topicConfigs); + + System.out.printf("submit batch of topic config to %s success, please check the result later.%n", + brokerAddress); + return; + + } else if (commandLine.hasOption('c')) { + final String clusterName = commandLine.getOptionValue('c').trim(); + + defaultMQAdminExt.start(); + + Set masterSet = + CommandUtil.fetchMasterAddrByClusterName(defaultMQAdminExt, clusterName); + for (String brokerAddress : masterSet) { + defaultMQAdminExt.createAndUpdateTopicConfigList(brokerAddress, topicConfigs); + + System.out.printf("submit batch of topic config to %s success, please check the result later.%n", + brokerAddress); + } + } + + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/topic/UpdateTopicPermSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/UpdateTopicPermSubCommand.java new file mode 100644 index 0000000..d27cd18 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/UpdateTopicPermSubCommand.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.tools.command.topic; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.remoting.RPCHook; +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.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class UpdateTopicPermSubCommand implements SubCommand { + + @Override + public String commandName() { + return "updateTopicPerm"; + } + + @Override + public String commandDesc() { + return "Update topic perm."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + Option opt = new Option("b", "brokerAddr", true, "create topic to which broker"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "clusterName", true, "create topic to which cluster"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("t", "topic", true, "topic name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("p", "perm", true, "set topic's permission(2|4|6), intro[2:W; 4:R; 6:RW]"); + opt.setRequired(true); + options.addOption(opt); + + return options; + } + + @Override + public void execute(final CommandLine commandLine, final Options options, + RPCHook rpcHook) throws SubCommandException { + + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + defaultMQAdminExt.start(); + + TopicConfig topicConfig = new TopicConfig(); + String topic; + if (commandLine.hasOption('t')) { + topic = commandLine.getOptionValue('t').trim(); + } else { + System.out.printf("topic parameter value must be need.%n"); + return; + } + TopicRouteData topicRouteData = defaultMQAdminExt.examineTopicRouteInfo(topic); + assert topicRouteData != null; + List queueDatas = topicRouteData.getQueueDatas(); + assert queueDatas != null && queueDatas.size() > 0; + QueueData queueData = queueDatas.get(0); + topicConfig.setTopicName(topic); + topicConfig.setWriteQueueNums(queueData.getWriteQueueNums()); + topicConfig.setReadQueueNums(queueData.getReadQueueNums()); + topicConfig.setTopicSysFlag(queueData.getTopicSysFlag()); + //new perm + int perm; + if (commandLine.hasOption('p')) { + perm = Integer.parseInt(commandLine.getOptionValue('p').trim()); + } else { + System.out.printf("perm parameter value must be need.%n"); + return; + } + topicConfig.setPerm(perm); + if (commandLine.hasOption('b')) { + String brokerAddr = commandLine.getOptionValue('b').trim(); + List brokerDatas = topicRouteData.getBrokerDatas(); + String brokerName = null; + for (BrokerData data : brokerDatas) { + HashMap brokerAddrs = data.getBrokerAddrs(); + if (brokerAddrs == null || brokerAddrs.size() == 0) { + continue; + } + for (Map.Entry entry : brokerAddrs.entrySet()) { + if (brokerAddr.equals(entry.getValue()) && MixAll.MASTER_ID == entry.getKey()) { + brokerName = data.getBrokerName(); + break; + } + } + if (brokerName != null) { + break; + } + } + + if (brokerName != null) { + List queueDataList = topicRouteData.getQueueDatas(); + assert queueDataList != null && queueDataList.size() > 0; + int oldPerm = 0; + for (QueueData data : queueDataList) { + if (brokerName.equals(data.getBrokerName())) { + oldPerm = data.getPerm(); + if (perm == oldPerm) { + System.out.printf("new perm equals to the old one!%n"); + return; + } + break; + } + } + defaultMQAdminExt.createAndUpdateTopicConfig(brokerAddr, topicConfig); + System.out.printf("update topic perm from %s to %s in %s success.%n", oldPerm, perm, brokerAddr); + System.out.printf("%s.%n", topicConfig); + return; + } else { + System.out.printf("updateTopicPerm error broker not exit or broker is not master!.%n"); + return; + } + + } else if (commandLine.hasOption('c')) { + String clusterName = commandLine.getOptionValue('c').trim(); + Set masterSet = + CommandUtil.fetchMasterAddrByClusterName(defaultMQAdminExt, clusterName); + for (String addr : masterSet) { + defaultMQAdminExt.createAndUpdateTopicConfig(addr, topicConfig); + System.out.printf("update topic perm from %s to %s in %s success.%n", queueData.getPerm(), perm, addr); + } + return; + } + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/command/topic/UpdateTopicSubCommand.java b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/UpdateTopicSubCommand.java new file mode 100644 index 0000000..2989141 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/command/topic/UpdateTopicSubCommand.java @@ -0,0 +1,204 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tools.command.topic; + +import java.util.Map; +import java.util.Set; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionGroup; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.attribute.AttributeParser; +import org.apache.rocketmq.common.sysflag.TopicSysFlag; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.command.CommandUtil; +import org.apache.rocketmq.tools.command.SubCommand; +import org.apache.rocketmq.tools.command.SubCommandException; + +public class UpdateTopicSubCommand implements SubCommand { + + @Override + public String commandName() { + return "updateTopic"; + } + + @Override + public String commandDesc() { + return "Update or create topic."; + } + + @Override + public Options buildCommandlineOptions(Options options) { + OptionGroup optionGroup = new OptionGroup(); + + Option opt = new Option("b", "brokerAddr", true, "create topic to which broker"); + optionGroup.addOption(opt); + + opt = new Option("c", "clusterName", true, "create topic to which cluster"); + optionGroup.addOption(opt); + + optionGroup.setRequired(true); + options.addOptionGroup(optionGroup); + + opt = new Option("a", "attributes", true, "attribute(+a=b,+c=d,-e)"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("t", "topic", true, "topic name"); + opt.setRequired(true); + options.addOption(opt); + + opt = new Option("r", "readQueueNums", true, "set read queue nums"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("w", "writeQueueNums", true, "set write queue nums"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("p", "perm", true, "set topic's permission(2|4|6), intro[2:W 4:R; 6:RW]"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("o", "order", true, "set topic's order(true|false)"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("u", "unit", true, "is unit topic (true|false)"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("s", "hasUnitSub", true, "has unit sub (true|false)"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + @Override + public void execute(final CommandLine commandLine, final Options options, + RPCHook rpcHook) throws SubCommandException { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis())); + + try { + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setReadQueueNums(8); + topicConfig.setWriteQueueNums(8); + topicConfig.setTopicName(commandLine.getOptionValue('t').trim()); + + if (commandLine.hasOption('a')) { + String attributesModification = commandLine.getOptionValue('a').trim(); + Map attributes = AttributeParser.parseToMap(attributesModification); + topicConfig.setAttributes(attributes); + } + + // readQueueNums + if (commandLine.hasOption('r')) { + topicConfig.setReadQueueNums(Integer.parseInt(commandLine.getOptionValue('r').trim())); + } + + // writeQueueNums + if (commandLine.hasOption('w')) { + topicConfig.setWriteQueueNums(Integer.parseInt(commandLine.getOptionValue('w').trim())); + } + + // perm + if (commandLine.hasOption('p')) { + topicConfig.setPerm(Integer.parseInt(commandLine.getOptionValue('p').trim())); + } + + boolean isUnit = false; + if (commandLine.hasOption('u')) { + isUnit = Boolean.parseBoolean(commandLine.getOptionValue('u').trim()); + } + + boolean isCenterSync = false; + if (commandLine.hasOption('s')) { + isCenterSync = Boolean.parseBoolean(commandLine.getOptionValue('s').trim()); + } + + int topicCenterSync = TopicSysFlag.buildSysFlag(isUnit, isCenterSync); + topicConfig.setTopicSysFlag(topicCenterSync); + + boolean isOrder = false; + if (commandLine.hasOption('o')) { + isOrder = Boolean.parseBoolean(commandLine.getOptionValue('o').trim()); + } + topicConfig.setOrder(isOrder); + + if (commandLine.hasOption('b')) { + String addr = commandLine.getOptionValue('b').trim(); + + defaultMQAdminExt.start(); + defaultMQAdminExt.createAndUpdateTopicConfig(addr, topicConfig); + + if (isOrder) { + String brokerName = CommandUtil.fetchBrokerNameByAddr(defaultMQAdminExt, addr); + String orderConf = brokerName + ":" + topicConfig.getWriteQueueNums(); + defaultMQAdminExt.createOrUpdateOrderConf(topicConfig.getTopicName(), orderConf, false); + System.out.printf("%s%n", String.format("set broker orderConf. isOrder=%s, orderConf=[%s]", + isOrder, orderConf.toString())); + } + System.out.printf("create topic to %s success.%n", addr); + System.out.printf("%s%n", topicConfig); + return; + + } else if (commandLine.hasOption('c')) { + String clusterName = commandLine.getOptionValue('c').trim(); + + defaultMQAdminExt.start(); + + Set masterSet = + CommandUtil.fetchMasterAddrByClusterName(defaultMQAdminExt, clusterName); + for (String addr : masterSet) { + defaultMQAdminExt.createAndUpdateTopicConfig(addr, topicConfig); + System.out.printf("create topic to %s success.%n", addr); + } + + if (isOrder) { + Set brokerNameSet = + CommandUtil.fetchBrokerNameByClusterName(defaultMQAdminExt, clusterName); + StringBuilder orderConf = new StringBuilder(); + String splitor = ""; + for (String s : brokerNameSet) { + orderConf.append(splitor).append(s).append(":") + .append(topicConfig.getWriteQueueNums()); + splitor = ";"; + } + defaultMQAdminExt.createOrUpdateOrderConf(topicConfig.getTopicName(), + orderConf.toString(), true); + System.out.printf("set cluster orderConf. isOrder=%s, orderConf=[%s]%n", isOrder, orderConf); + } + + System.out.printf("%s%n", topicConfig); + return; + } + + ServerUtil.printCommandLineHelp("mqadmin " + this.commandName(), options); + } catch (Exception e) { + throw new SubCommandException(this.getClass().getSimpleName() + " command failed", e); + } finally { + defaultMQAdminExt.shutdown(); + } + } + +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/monitor/DefaultMonitorListener.java b/tools/src/main/java/org/apache/rocketmq/tools/monitor/DefaultMonitorListener.java new file mode 100644 index 0000000..7ef6e31 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/monitor/DefaultMonitorListener.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.tools.monitor; + +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.TreeMap; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class DefaultMonitorListener implements MonitorListener { + private final static String LOG_PREFIX = "[MONITOR] "; + private final static String LOG_NOTIFY = LOG_PREFIX + " [NOTIFY] "; + private final Logger logger = LoggerFactory.getLogger(DefaultMonitorListener.class); + + public DefaultMonitorListener() { + } + + @Override + public void beginRound() { + logger.info(LOG_PREFIX + "=========================================beginRound"); + } + + @Override + public void reportUndoneMsgs(UndoneMsgs undoneMsgs) { + logger.info(String.format(LOG_PREFIX + "reportUndoneMsgs: %s", undoneMsgs)); + } + + @Override + public void reportFailedMsgs(FailedMsgs failedMsgs) { + logger.info(String.format(LOG_PREFIX + "reportFailedMsgs: %s", failedMsgs)); + } + + @Override + public void reportDeleteMsgsEvent(DeleteMsgsEvent deleteMsgsEvent) { + logger.info(String.format(LOG_PREFIX + "reportDeleteMsgsEvent: %s", deleteMsgsEvent)); + } + + @Override + public void reportConsumerRunningInfo(TreeMap criTable) { + + { + boolean result = ConsumerRunningInfo.analyzeSubscription(criTable); + if (!result) { + logger.info(String.format(LOG_NOTIFY + + "reportConsumerRunningInfo: ConsumerGroup: %s, Subscription different", criTable + .firstEntry().getValue().getProperties().getProperty("consumerGroup"))); + } + } + + { + Iterator> it = criTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + String result = ConsumerRunningInfo.analyzeProcessQueue(next.getKey(), next.getValue()); + if (!result.isEmpty()) { + logger.info(String.format(LOG_NOTIFY + + "reportConsumerRunningInfo: ConsumerGroup: %s, ClientId: %s, %s", + criTable.firstEntry().getValue().getProperties().getProperty("consumerGroup"), + next.getKey(), + result)); + } + } + } + } + + @Override + public void endRound() { + logger.info(LOG_PREFIX + "=========================================endRound"); + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/monitor/DeleteMsgsEvent.java b/tools/src/main/java/org/apache/rocketmq/tools/monitor/DeleteMsgsEvent.java new file mode 100644 index 0000000..2054c24 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/monitor/DeleteMsgsEvent.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.tools.monitor; + +import org.apache.rocketmq.remoting.protocol.topic.OffsetMovedEvent; + +public class DeleteMsgsEvent { + private OffsetMovedEvent offsetMovedEvent; + private long eventTimestamp; + + public OffsetMovedEvent getOffsetMovedEvent() { + return offsetMovedEvent; + } + + public void setOffsetMovedEvent(OffsetMovedEvent offsetMovedEvent) { + this.offsetMovedEvent = offsetMovedEvent; + } + + public long getEventTimestamp() { + return eventTimestamp; + } + + public void setEventTimestamp(long eventTimestamp) { + this.eventTimestamp = eventTimestamp; + } + + @Override + public String toString() { + return "DeleteMsgsEvent [offsetMovedEvent=" + offsetMovedEvent + ", eventTimestamp=" + eventTimestamp + + "]"; + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/monitor/FailedMsgs.java b/tools/src/main/java/org/apache/rocketmq/tools/monitor/FailedMsgs.java new file mode 100644 index 0000000..4938987 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/monitor/FailedMsgs.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.tools.monitor; + +public class FailedMsgs { + private String consumerGroup; + private String topic; + private long failedMsgsTotalRecently; + + 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 long getFailedMsgsTotalRecently() { + return failedMsgsTotalRecently; + } + + public void setFailedMsgsTotalRecently(long failedMsgsTotalRecently) { + this.failedMsgsTotalRecently = failedMsgsTotalRecently; + } + + @Override + public String toString() { + return "FailedMsgs [consumerGroup=" + consumerGroup + ", topic=" + topic + + ", failedMsgsTotalRecently=" + failedMsgsTotalRecently + "]"; + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/monitor/MonitorConfig.java b/tools/src/main/java/org/apache/rocketmq/tools/monitor/MonitorConfig.java new file mode 100644 index 0000000..f4a95a7 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/monitor/MonitorConfig.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.tools.monitor; + +import org.apache.rocketmq.common.MixAll; + +public class MonitorConfig { + private String namesrvAddr = System.getProperty(MixAll.NAMESRV_ADDR_PROPERTY, + System.getenv(MixAll.NAMESRV_ADDR_ENV)); + + private int roundInterval = 1000 * 60; + + public String getNamesrvAddr() { + return namesrvAddr; + } + + public void setNamesrvAddr(String namesrvAddr) { + this.namesrvAddr = namesrvAddr; + } + + public int getRoundInterval() { + return roundInterval; + } + + public void setRoundInterval(int roundInterval) { + this.roundInterval = roundInterval; + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/monitor/MonitorListener.java b/tools/src/main/java/org/apache/rocketmq/tools/monitor/MonitorListener.java new file mode 100644 index 0000000..673d2c7 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/monitor/MonitorListener.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.tools.monitor; + +import java.util.TreeMap; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; + +public interface MonitorListener { + void beginRound(); + + void reportUndoneMsgs(UndoneMsgs undoneMsgs); + + void reportFailedMsgs(FailedMsgs failedMsgs); + + void reportDeleteMsgsEvent(DeleteMsgsEvent deleteMsgsEvent); + + void reportConsumerRunningInfo(TreeMap criTable); + + void endRound(); +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/monitor/MonitorService.java b/tools/src/main/java/org/apache/rocketmq/tools/monitor/MonitorService.java new file mode 100644 index 0000000..b66dfad --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/monitor/MonitorService.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.tools.monitor; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; +import java.util.TreeMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.RandomUtils; +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.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.ThreadFactoryImpl; +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.admin.ConsumeStats; +import org.apache.rocketmq.remoting.protocol.admin.OffsetWrapper; +import org.apache.rocketmq.remoting.protocol.body.Connection; +import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.body.TopicList; +import org.apache.rocketmq.remoting.protocol.topic.OffsetMovedEvent; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; + +public class MonitorService { + private final Logger logger = LoggerFactory.getLogger(MonitorService.class); + private final ScheduledExecutorService scheduledExecutorService = Executors + .newSingleThreadScheduledExecutor(new ThreadFactoryImpl("MonitorService")); + + private final MonitorConfig monitorConfig; + + private final MonitorListener monitorListener; + + private final DefaultMQAdminExt defaultMQAdminExt; + private final DefaultMQPullConsumer defaultMQPullConsumer = new DefaultMQPullConsumer( + MixAll.TOOLS_CONSUMER_GROUP); + private final DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer( + MixAll.MONITOR_CONSUMER_GROUP); + + public MonitorService(MonitorConfig monitorConfig, MonitorListener monitorListener, RPCHook rpcHook) { + this.monitorConfig = monitorConfig; + this.monitorListener = monitorListener; + + this.defaultMQAdminExt = new DefaultMQAdminExt(rpcHook); + this.defaultMQAdminExt.setInstanceName(instanceName()); + this.defaultMQAdminExt.setNamesrvAddr(monitorConfig.getNamesrvAddr()); + + this.defaultMQPullConsumer.setInstanceName(instanceName()); + this.defaultMQPullConsumer.setNamesrvAddr(monitorConfig.getNamesrvAddr()); + + this.defaultMQPushConsumer.setInstanceName(instanceName()); + this.defaultMQPushConsumer.setNamesrvAddr(monitorConfig.getNamesrvAddr()); + try { + this.defaultMQPushConsumer.setConsumeThreadMin(1); + this.defaultMQPushConsumer.setConsumeThreadMax(1); + this.defaultMQPushConsumer.subscribe(TopicValidator.RMQ_SYS_OFFSET_MOVED_EVENT, "*"); + this.defaultMQPushConsumer.registerMessageListener(new MessageListenerConcurrently() { + + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, + ConsumeConcurrentlyContext context) { + try { + OffsetMovedEvent ome = + OffsetMovedEvent.decode(msgs.get(0).getBody(), OffsetMovedEvent.class); + + DeleteMsgsEvent deleteMsgsEvent = new DeleteMsgsEvent(); + deleteMsgsEvent.setOffsetMovedEvent(ome); + deleteMsgsEvent.setEventTimestamp(msgs.get(0).getStoreTimestamp()); + + MonitorService.this.monitorListener.reportDeleteMsgsEvent(deleteMsgsEvent); + } catch (Exception e) { + } + + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + } catch (MQClientException e) { + } + } + + public static void main(String[] args) throws MQClientException { + main0(args, null); + } + + public static void main0(String[] args, RPCHook rpcHook) throws MQClientException { + final MonitorService monitorService = + new MonitorService(new MonitorConfig(), new DefaultMonitorListener(), rpcHook); + monitorService.start(); + + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + private volatile boolean hasShutdown = false; + + @Override + public void run() { + synchronized (this) { + if (!this.hasShutdown) { + this.hasShutdown = true; + monitorService.shutdown(); + } + } + } + }, "ShutdownHook")); + } + + private String instanceName() { + final int randomInteger = RandomUtils.nextInt(0, Integer.MAX_VALUE); + String name = System.currentTimeMillis() + randomInteger + this.monitorConfig.getNamesrvAddr(); + return "MonitorService_" + name.hashCode(); + } + + public void start() throws MQClientException { + this.defaultMQPullConsumer.start(); + this.defaultMQAdminExt.start(); + this.defaultMQPushConsumer.start(); + this.startScheduleTask(); + } + + public void shutdown() { + this.defaultMQPullConsumer.shutdown(); + this.defaultMQAdminExt.shutdown(); + this.defaultMQPushConsumer.shutdown(); + } + + private void startScheduleTask() { + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + MonitorService.this.doMonitorWork(); + } catch (Exception e) { + logger.error("doMonitorWork Exception", e); + } + } + }, 1000 * 20, this.monitorConfig.getRoundInterval(), TimeUnit.MILLISECONDS); + } + + public void doMonitorWork() throws RemotingException, MQClientException, InterruptedException { + long beginTime = System.currentTimeMillis(); + this.monitorListener.beginRound(); + + TopicList topicList = defaultMQAdminExt.fetchAllTopicList(); + for (String topic : topicList.getTopicList()) { + if (topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + String consumerGroup = KeyBuilder.parseGroup(topic); + + try { + this.reportUndoneMsgs(consumerGroup); + } catch (Exception e) { + // log.error("reportUndoneMsgs Exception", e); + } + + try { + this.reportConsumerRunningInfo(consumerGroup); + } catch (Exception e) { + // log.error("reportConsumerRunningInfo Exception", e); + } + } + } + this.monitorListener.endRound(); + long spentTimeMills = System.currentTimeMillis() - beginTime; + logger.info("Execute one round monitor work, spent timemills: {}", spentTimeMills); + } + + private void reportUndoneMsgs(final String consumerGroup) { + ConsumeStats cs = null; + try { + cs = defaultMQAdminExt.examineConsumeStats(consumerGroup); + } catch (Exception ignore) { + return; + } + + ConsumerConnection cc = null; + try { + cc = defaultMQAdminExt.examineConsumerConnectionInfo(consumerGroup); + } catch (Exception ignore) { + return; + } + + if (cs != null) { + + HashMap csByTopic = new HashMap<>(); + { + Iterator> it = cs.getOffsetTable().entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + MessageQueue mq = next.getKey(); + OffsetWrapper ow = next.getValue(); + ConsumeStats csTmp = csByTopic.get(mq.getTopic()); + if (null == csTmp) { + csTmp = new ConsumeStats(); + csByTopic.put(mq.getTopic(), csTmp); + } + + csTmp.getOffsetTable().put(mq, ow); + } + } + + { + Iterator> it = csByTopic.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + UndoneMsgs undoneMsgs = new UndoneMsgs(); + undoneMsgs.setConsumerGroup(consumerGroup); + undoneMsgs.setTopic(next.getKey()); + this.computeUndoneMsgs(undoneMsgs, next.getValue()); + this.monitorListener.reportUndoneMsgs(undoneMsgs); + this.reportFailedMsgs(consumerGroup, next.getKey()); + } + } + } + } + + public void reportConsumerRunningInfo(final String consumerGroup) throws InterruptedException, + MQBrokerException, RemotingException, MQClientException { + ConsumerConnection cc = defaultMQAdminExt.examineConsumerConnectionInfo(consumerGroup); + TreeMap infoMap = new TreeMap<>(); + for (Connection c : cc.getConnectionSet()) { + String clientId = c.getClientId(); + + if (c.getVersion() < MQVersion.Version.V3_1_8_SNAPSHOT.ordinal()) { + continue; + } + + try { + ConsumerRunningInfo info = + defaultMQAdminExt.getConsumerRunningInfo(consumerGroup, clientId, false); + infoMap.put(clientId, info); + } catch (Exception e) { + } + } + + if (!infoMap.isEmpty()) { + this.monitorListener.reportConsumerRunningInfo(infoMap); + } + } + + private void computeUndoneMsgs(final UndoneMsgs undoneMsgs, final ConsumeStats consumeStats) { + long total = 0; + long singleMax = 0; + long delayMax = 0; + Iterator> it = consumeStats.getOffsetTable().entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + MessageQueue mq = next.getKey(); + OffsetWrapper ow = next.getValue(); + long diff = ow.getBrokerOffset() - ow.getConsumerOffset(); + + if (diff > singleMax) { + singleMax = diff; + } + + if (diff > 0) { + total += diff; + } + + // Delay + if (ow.getLastTimestamp() > 0) { + try { + long maxOffset = this.defaultMQPullConsumer.maxOffset(mq); + if (maxOffset > 0) { + PullResult pull = this.defaultMQPullConsumer.pull(mq, "*", maxOffset - 1, 1); + switch (pull.getPullStatus()) { + case FOUND: + long delay = + pull.getMsgFoundList().get(0).getStoreTimestamp() - ow.getLastTimestamp(); + if (delay > delayMax) { + delayMax = delay; + } + break; + case NO_MATCHED_MSG: + case NO_NEW_MSG: + case OFFSET_ILLEGAL: + break; + default: + break; + } + } + } catch (Exception e) { + } + } + } + + undoneMsgs.setUndoneMsgsTotal(total); + undoneMsgs.setUndoneMsgsSingleMQ(singleMax); + undoneMsgs.setUndoneMsgsDelayTimeMills(delayMax); + } + + private void reportFailedMsgs(final String consumerGroup, final String topic) { + + } +} diff --git a/tools/src/main/java/org/apache/rocketmq/tools/monitor/UndoneMsgs.java b/tools/src/main/java/org/apache/rocketmq/tools/monitor/UndoneMsgs.java new file mode 100644 index 0000000..ea38190 --- /dev/null +++ b/tools/src/main/java/org/apache/rocketmq/tools/monitor/UndoneMsgs.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.tools.monitor; + +public class UndoneMsgs { + private String consumerGroup; + private String topic; + + private long undoneMsgsTotal; + + private long undoneMsgsSingleMQ; + + private long undoneMsgsDelayTimeMills; + + 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 long getUndoneMsgsTotal() { + return undoneMsgsTotal; + } + + public void setUndoneMsgsTotal(long undoneMsgsTotal) { + this.undoneMsgsTotal = undoneMsgsTotal; + } + + public long getUndoneMsgsSingleMQ() { + return undoneMsgsSingleMQ; + } + + public void setUndoneMsgsSingleMQ(long undoneMsgsSingleMQ) { + this.undoneMsgsSingleMQ = undoneMsgsSingleMQ; + } + + public long getUndoneMsgsDelayTimeMills() { + return undoneMsgsDelayTimeMills; + } + + public void setUndoneMsgsDelayTimeMills(long undoneMsgsDelayTimeMills) { + this.undoneMsgsDelayTimeMills = undoneMsgsDelayTimeMills; + } + + @Override + public String toString() { + return "UndoneMsgs [consumerGroup=" + consumerGroup + ", topic=" + topic + ", undoneMsgsTotal=" + + undoneMsgsTotal + ", undoneMsgsSingleMQ=" + undoneMsgsSingleMQ + + ", undoneMsgsDelayTimeMills=" + undoneMsgsDelayTimeMills + "]"; + } +} diff --git a/tools/src/main/resources/rmq.tools.logback.xml b/tools/src/main/resources/rmq.tools.logback.xml new file mode 100644 index 0000000..c630219 --- /dev/null +++ b/tools/src/main/resources/rmq.tools.logback.xml @@ -0,0 +1,91 @@ + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}tools_default.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}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}${file.separator}logs${file.separator}rocketmqlogs${file.separator}tools.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}tools.%i.log.gz + 1 + 5 + + + 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/tools/src/test/java/org/apache/rocketmq/tools/admin/DefaultMQAdminExtTest.java b/tools/src/test/java/org/apache/rocketmq/tools/admin/DefaultMQAdminExtTest.java new file mode 100644 index 0000000..ec5f757 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/admin/DefaultMQAdminExtTest.java @@ -0,0 +1,550 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tools.admin; + +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +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.impl.MQClientAPIImpl; +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.TopicConfig; +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.common.namesrv.NamesrvUtil; +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.protocol.LanguageCode; +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.Connection; +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.GroupList; +import org.apache.rocketmq.remoting.protocol.body.KVTable; +import org.apache.rocketmq.remoting.protocol.body.ProducerConnection; +import org.apache.rocketmq.remoting.protocol.body.ProducerInfo; +import org.apache.rocketmq.remoting.protocol.body.ProducerTableInfo; +import org.apache.rocketmq.remoting.protocol.body.QueueTimeSpan; +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.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicConfigAndQueueMapping; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.tools.admin.api.MessageTrack; +import org.apache.rocketmq.tools.admin.api.TrackType; +import org.assertj.core.util.Maps; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +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.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.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class DefaultMQAdminExtTest { + private static final String BROKER1_ADDR = "127.0.0.1:10911"; + private static final String BROKER1_NAME = "default-broker"; + private static final String CLUSTER = "default-cluster"; + private static final String BROKER2_NAME = "broker-test"; + private static final String BROKER2_ADDR = "127.0.0.2:10911"; + private static final String TOPIC1 = "topic_one"; + private static final String TOPIC2 = "topic_two"; + private static DefaultMQAdminExt defaultMQAdminExt; + private static DefaultMQAdminExtImpl defaultMQAdminExtImpl; + private static MQClientInstance mqClientInstance = MQClientManager.getInstance().getOrCreateMQClientInstance(new ClientConfig()); + private static MQClientAPIImpl mQClientAPIImpl; + private static Properties properties = new Properties(); + private static TopicList topicList = new TopicList(); + private static TopicRouteData topicRouteData = new TopicRouteData(); + private static KVTable kvTable = new KVTable(); + private static ClusterInfo clusterInfo = new ClusterInfo(); + + @BeforeClass + public static void init() throws Exception { + mQClientAPIImpl = mock(MQClientAPIImpl.class); + defaultMQAdminExt = new DefaultMQAdminExt(); + defaultMQAdminExtImpl = new DefaultMQAdminExtImpl(defaultMQAdminExt, 1000); + + Field field = DefaultMQAdminExtImpl.class.getDeclaredField("mqClientInstance"); + field.setAccessible(true); + field.set(defaultMQAdminExtImpl, mqClientInstance); + field = MQClientInstance.class.getDeclaredField("mQClientAPIImpl"); + field.setAccessible(true); + field.set(mqClientInstance, mQClientAPIImpl); + field = DefaultMQAdminExt.class.getDeclaredField("defaultMQAdminExtImpl"); + field.setAccessible(true); + field.set(defaultMQAdminExt, defaultMQAdminExtImpl); + + properties.setProperty("maxMessageSize", "5000000"); + properties.setProperty("flushDelayOffsetInterval", "15000"); + properties.setProperty("serverSocketRcvBufSize", "655350"); + when(mQClientAPIImpl.getBrokerConfig(anyString(), anyLong())).thenReturn(properties); + + Set topicSet = new HashSet<>(); + topicSet.add(TOPIC1); + topicSet.add(TOPIC2); + topicList.setTopicList(topicSet); + when(mQClientAPIImpl.getTopicListFromNameServer(anyLong())).thenReturn(topicList); + + List brokerDatas = new ArrayList<>(); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(MixAll.MASTER_ID, BROKER1_ADDR); + BrokerData brokerData = new BrokerData(); + brokerData.setCluster(CLUSTER); + brokerData.setBrokerName(BROKER1_NAME); + brokerData.setBrokerAddrs(brokerAddrs); + brokerDatas.add(brokerData); + brokerDatas.add(new BrokerData(CLUSTER, BROKER2_NAME, (HashMap) Maps.newHashMap(MixAll.MASTER_ID, BROKER2_ADDR))); + topicRouteData.setBrokerDatas(brokerDatas); + topicRouteData.setQueueDatas(new ArrayList<>()); + topicRouteData.setFilterServerTable(new HashMap<>()); + when(mQClientAPIImpl.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(topicRouteData); + + HashMap result = new HashMap<>(); + result.put("id", String.valueOf(MixAll.MASTER_ID)); + result.put("brokerName", BROKER1_NAME); + kvTable.setTable(result); + when(mQClientAPIImpl.getBrokerRuntimeInfo(anyString(), anyLong())).thenReturn(kvTable); + + HashMap brokerAddrTable = new HashMap<>(); + brokerAddrTable.put(BROKER1_NAME, brokerData); + brokerAddrTable.put(BROKER2_NAME, new BrokerData()); + clusterInfo.setBrokerAddrTable(brokerAddrTable); + clusterInfo.setClusterAddrTable(new HashMap<>()); + when(mQClientAPIImpl.getBrokerClusterInfo(anyLong())).thenReturn(clusterInfo); + when(mQClientAPIImpl.cleanExpiredConsumeQueue(anyString(), anyLong())).thenReturn(true); + + Set clusterList = new HashSet<>(); + clusterList.add("default-cluster-one"); + clusterList.add("default-cluster-two"); + when(mQClientAPIImpl.getClusterList(anyString(), anyLong())).thenReturn(clusterList); + + GroupList groupList = new GroupList(); + HashSet groups = new HashSet<>(); + groups.add("consumer-group-one"); + groups.add("consumer-group-two"); + groupList.setGroupList(groups); + when(mQClientAPIImpl.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(topicRouteData); + when(mQClientAPIImpl.queryTopicConsumeByWho(anyString(), anyString(), anyLong())).thenReturn(groupList); + + 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); + when(mQClientAPIImpl.getAllSubscriptionGroup(anyString(), anyLong())).thenReturn(subscriptionGroupWrapper); + + String topicListConfig = "topicListConfig"; + when(mQClientAPIImpl.getKVConfigValue(anyString(), anyString(), anyLong())).thenReturn(topicListConfig); + + KVTable kvTable = new KVTable(); + HashMap kv = new HashMap<>(); + kv.put("broker-name", "broker-one"); + kv.put("cluster-name", "default-cluster"); + kvTable.setTable(kv); + when(mQClientAPIImpl.getKVListByNamespace(anyString(), anyLong())).thenReturn(kvTable); + +// ConsumeStats consumeStats = new ConsumeStats(); +// consumeStats.setConsumeTps(1234); +// MessageQueue messageQueue = new MessageQueue(); +// OffsetWrapper offsetWrapper = new OffsetWrapper(); +// HashMap stats = new HashMap<>(); +// stats.put(messageQueue, offsetWrapper); +// consumeStats.setOffsetTable(stats); +// when(mQClientAPIImpl.getConsumeStats(anyString(), anyString(), anyString(), anyLong())).thenReturn(consumeStats); + + ConsumerConnection consumerConnection = new ConsumerConnection(); + consumerConnection.setConsumeType(ConsumeType.CONSUME_PASSIVELY); + consumerConnection.setMessageModel(MessageModel.CLUSTERING); + HashSet connections = new HashSet<>(); + connections.add(new Connection()); + consumerConnection.setConnectionSet(connections); + consumerConnection.setSubscriptionTable(new ConcurrentHashMap<>()); + consumerConnection.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + when(mQClientAPIImpl.getConsumerConnectionList(anyString(), anyString(), anyLong())).thenReturn(consumerConnection); + + ProducerConnection producerConnection = new ProducerConnection(); + Connection connection = new Connection(); + connection.setClientAddr("127.0.0.1:9898"); + connection.setClientId("PID_12345"); + HashSet connectionSet = new HashSet<>(); + connectionSet.add(connection); + producerConnection.setConnectionSet(connectionSet); + when(mQClientAPIImpl.getProducerConnectionList(anyString(), anyString(), anyLong())).thenReturn(producerConnection); + + ProducerTableInfo producerTableInfo = new ProducerTableInfo(new HashMap<>()); + producerTableInfo.getData().put("test-producer-group", Arrays.asList(new ProducerInfo( + "xxxx-client-id", + "127.0.0.1:18978", + LanguageCode.JAVA, + 400, + System.currentTimeMillis() + + ))); + when(mQClientAPIImpl.getAllProducerInfo(anyString(), anyLong())).thenReturn(producerTableInfo); + + when(mQClientAPIImpl.wipeWritePermOfBroker(anyString(), anyString(), anyLong())).thenReturn(6); + when(mQClientAPIImpl.addWritePermOfBroker(anyString(), anyString(), anyLong())).thenReturn(7); + + TopicStatsTable topicStatsTable = new TopicStatsTable(); + topicStatsTable.setOffsetTable(new HashMap<>()); + + Map> consumerStatus = new HashMap<>(); + when(mQClientAPIImpl.invokeBrokerToGetConsumerStatus(anyString(), anyString(), anyString(), anyString(), anyLong())).thenReturn(consumerStatus); + + List queueTimeSpanList = new ArrayList<>(); + when(mQClientAPIImpl.queryConsumeTimeSpan(anyString(), anyString(), anyString(), anyLong())).thenReturn(queueTimeSpanList); + + ConsumerRunningInfo consumerRunningInfo = new ConsumerRunningInfo(); + consumerRunningInfo.setJstack("test"); + consumerRunningInfo.setMqTable(new TreeMap<>()); + consumerRunningInfo.setStatusTable(new TreeMap<>()); + consumerRunningInfo.setSubscriptionSet(new TreeSet<>()); + when(mQClientAPIImpl.getConsumerRunningInfo(anyString(), anyString(), anyString(), anyBoolean(), anyLong())).thenReturn(consumerRunningInfo); + + TopicConfigSerializeWrapper topicConfigSerializeWrapper = new TopicConfigSerializeWrapper(); + topicConfigSerializeWrapper.setTopicConfigTable(new ConcurrentHashMap() { + { + put("topic_test_examine_topicConfig", new TopicConfig("topic_test_examine_topicConfig")); + } + }); + //when(mQClientAPIImpl.getAllTopicConfig(anyString(),anyLong())).thenReturn(topicConfigSerializeWrapper); + when(mQClientAPIImpl.getTopicConfig(anyString(), anyString(), anyLong())).thenReturn(new TopicConfigAndQueueMapping(new TopicConfig("topic_test_examine_topicConfig"), null)); + } + + @AfterClass + public static void terminate() throws Exception { + if (defaultMQAdminExtImpl != null) + defaultMQAdminExt.shutdown(); + } + + @Test + public void testUpdateBrokerConfig() throws InterruptedException, RemotingConnectException, UnsupportedEncodingException, RemotingTimeoutException, MQBrokerException, RemotingSendRequestException { + Properties result = defaultMQAdminExt.getBrokerConfig("127.0.0.1:10911"); + assertThat(result.getProperty("maxMessageSize")).isEqualTo("5000000"); + assertThat(result.getProperty("flushDelayOffsetInterval")).isEqualTo("15000"); + assertThat(result.getProperty("serverSocketRcvBufSize")).isEqualTo("655350"); + } + + @Test + public void testFetchAllTopicList() throws RemotingException, MQClientException, InterruptedException { + TopicList topicList = defaultMQAdminExt.fetchAllTopicList(); + assertThat(topicList.getTopicList().size()).isEqualTo(2); + assertThat(topicList.getTopicList()).contains("topic_one"); + } + + @Test + public void testFetchBrokerRuntimeStats() throws InterruptedException, MQBrokerException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException { + KVTable brokerStats = defaultMQAdminExt.fetchBrokerRuntimeStats("127.0.0.1:10911"); + assertThat(brokerStats.getTable().get("id")).isEqualTo(String.valueOf(MixAll.MASTER_ID)); + assertThat(brokerStats.getTable().get("brokerName")).isEqualTo("default-broker"); + } + + @Test + public void testExamineBrokerClusterInfo() throws InterruptedException, MQBrokerException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException { + ClusterInfo clusterInfo = defaultMQAdminExt.examineBrokerClusterInfo(); + Map brokerList = clusterInfo.getBrokerAddrTable(); + assertThat(brokerList.get("default-broker").getBrokerName()).isEqualTo("default-broker"); + assertThat(brokerList.containsKey("broker-test")).isTrue(); + + HashMap> clusterMap = new HashMap<>(); + Set brokers = new HashSet<>(); + brokers.add("default-broker"); + brokers.add("broker-test"); + clusterMap.put("default-cluster", brokers); + ClusterInfo cInfo = mock(ClusterInfo.class); + when(cInfo.getClusterAddrTable()).thenReturn(clusterMap); + Map> clusterAddress = cInfo.getClusterAddrTable(); + assertThat(clusterAddress.containsKey("default-cluster")).isTrue(); + assertThat(clusterAddress.get("default-cluster").size()).isEqualTo(2); + } + + @Ignore + @Test + public void testExamineConsumeStats() throws InterruptedException, RemotingException, MQClientException, MQBrokerException { + ConsumeStats consumeStats = defaultMQAdminExt.examineConsumeStats("default-consumer-group", "unit-test"); + assertThat(consumeStats.getConsumeTps()).isGreaterThanOrEqualTo(1234); + ConsumerConnection connection = new ConsumerConnection(); + connection.setMessageModel(MessageModel.BROADCASTING); + HashSet connections = new HashSet<>(); + connections.add(new Connection()); + connection.setConnectionSet(connections); + when(mQClientAPIImpl.getConsumeStats(anyString(), anyString(), anyString(), anyLong())) + .thenReturn(new ConsumeStats()); + when(mQClientAPIImpl.getConsumerConnectionList(anyString(), anyString(), anyLong())) + .thenReturn(new ConsumerConnection()).thenReturn(connection); + // CONSUMER_NOT_ONLINE + try { + defaultMQAdminExt.examineConsumeStats("default-consumer-group", "unit-test"); + } catch (Exception e) { + assertThat(e instanceof MQClientException).isTrue(); + assertThat(((MQClientException) e).getResponseCode()).isEqualTo(ResponseCode.CONSUMER_NOT_ONLINE); + } + // BROADCAST_CONSUMPTION + try { + defaultMQAdminExt.examineConsumeStats("default-consumer-group", "unit-test"); + } catch (Exception e) { + assertThat(e instanceof MQClientException).isTrue(); + assertThat(((MQClientException) e).getResponseCode()).isEqualTo(ResponseCode.BROADCAST_CONSUMPTION); + } + } + + @Test + public void testExamineConsumerConnectionInfo() throws InterruptedException, RemotingException, MQClientException, MQBrokerException { + ConsumerConnection consumerConnection = defaultMQAdminExt.examineConsumerConnectionInfo("default-consumer-group"); + assertThat(consumerConnection.getConsumeType()).isEqualTo(ConsumeType.CONSUME_PASSIVELY); + assertThat(consumerConnection.getMessageModel()).isEqualTo(MessageModel.CLUSTERING); + + consumerConnection = defaultMQAdminExt.examineConsumerConnectionInfo("default-consumer-group", "127.0.0.1:10911"); + assertThat(consumerConnection.getConsumeType()).isEqualTo(ConsumeType.CONSUME_PASSIVELY); + assertThat(consumerConnection.getMessageModel()).isEqualTo(MessageModel.CLUSTERING); + } + + @Test + public void testExamineProducerConnectionInfo() throws InterruptedException, RemotingException, MQClientException, MQBrokerException { + ProducerConnection producerConnection = defaultMQAdminExt.examineProducerConnectionInfo("default-producer-group", "unit-test"); + assertThat(producerConnection.getConnectionSet().size()).isEqualTo(1); + } + + @Test + public void testGetAllProducerInfo() throws InterruptedException, RemotingException, MQClientException, MQBrokerException { + ProducerTableInfo producerTableInfo = defaultMQAdminExt.getAllProducerInfo("127.0.0.1:10911"); + assertThat(producerTableInfo.getData().size()).isEqualTo(1); + } + + + @Test + public void testWipeWritePermOfBroker() throws InterruptedException, RemotingCommandException, RemotingSendRequestException, RemotingTimeoutException, MQClientException, RemotingConnectException { + int result = defaultMQAdminExt.wipeWritePermOfBroker("127.0.0.1:9876", "default-broker"); + assertThat(result).isEqualTo(6); + } + + @Test + public void testAddWritePermOfBroker() throws InterruptedException, RemotingCommandException, RemotingSendRequestException, RemotingTimeoutException, MQClientException, RemotingConnectException { + int result = defaultMQAdminExt.addWritePermOfBroker("127.0.0.1:9876", "default-broker"); + assertThat(result).isEqualTo(7); + } + + @Test + public void testExamineTopicRouteInfo() throws RemotingException, MQClientException, InterruptedException { + TopicRouteData topicRouteData = defaultMQAdminExt.examineTopicRouteInfo("UnitTest"); + assertThat(topicRouteData.getBrokerDatas().get(0).getBrokerName()).isEqualTo("default-broker"); + assertThat(topicRouteData.getBrokerDatas().get(0).getCluster()).isEqualTo("default-cluster"); + } + + @Test + public void testGetNameServerAddressList() { + List result = new ArrayList<>(); + result.add("default-name-one"); + result.add("default-name-two"); + when(mqClientInstance.getMQClientAPIImpl().getNameServerAddressList()).thenReturn(result); + List nameList = defaultMQAdminExt.getNameServerAddressList(); + assertThat(nameList.get(0)).isEqualTo("default-name-one"); + assertThat(nameList.get(1)).isEqualTo("default-name-two"); + } + + @Test + public void testPutKVConfig() throws RemotingException, MQClientException, InterruptedException { + String topicConfig = defaultMQAdminExt.getKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG, "UnitTest"); + assertThat(topicConfig).isEqualTo("topicListConfig"); + KVTable kvs = defaultMQAdminExt.getKVListByNamespace(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG); + assertThat(kvs.getTable().get("broker-name")).isEqualTo("broker-one"); + assertThat(kvs.getTable().get("cluster-name")).isEqualTo("default-cluster"); + } + + @Test + public void testQueryTopicConsumeByWho() throws InterruptedException, RemotingException, MQClientException, MQBrokerException { + GroupList groupList = defaultMQAdminExt.queryTopicConsumeByWho("UnitTest"); + assertThat(groupList.getGroupList().contains("consumer-group-two")).isTrue(); + } + + @Test + public void testQueryConsumeTimeSpan() throws InterruptedException, RemotingException, MQClientException, MQBrokerException { + List result = defaultMQAdminExt.queryConsumeTimeSpan("unit-test", "default-broker-group"); + assertThat(result.size()).isEqualTo(0); + } + + @Test + public void testCleanExpiredConsumerQueue() throws InterruptedException, RemotingTimeoutException, MQClientException, RemotingSendRequestException, RemotingConnectException { + boolean result = defaultMQAdminExt.cleanExpiredConsumerQueue("default-cluster"); + assertThat(result).isFalse(); + } + + @Test + public void testCleanExpiredConsumerQueueByAddr() throws InterruptedException, RemotingTimeoutException, MQClientException, RemotingSendRequestException, RemotingConnectException { + boolean clean = defaultMQAdminExt.cleanExpiredConsumerQueueByAddr("127.0.0.1:10911"); + assertThat(clean).isTrue(); + } + + @Test + public void testCleanUnusedTopic() throws InterruptedException, RemotingTimeoutException, MQClientException, RemotingSendRequestException, RemotingConnectException { + boolean result = defaultMQAdminExt.cleanUnusedTopic("default-cluster"); + assertThat(result).isFalse(); + } + + @Test + public void testGetConsumerRunningInfo() throws RemotingException, MQClientException, InterruptedException { + ConsumerRunningInfo consumerRunningInfo = defaultMQAdminExt.getConsumerRunningInfo("consumer-group", "cid_123", false); + assertThat(consumerRunningInfo.getJstack()).isEqualTo("test"); + } + + @Test + public void testMessageTrackDetail() throws InterruptedException, RemotingException, MQClientException, MQBrokerException { + MessageExt messageExt = new MessageExt(); + messageExt.setMsgId("msgId"); + messageExt.setTopic("unit-test"); + List messageTrackList = defaultMQAdminExt.messageTrackDetail(messageExt); + assertThat(messageTrackList.size()).isEqualTo(2); + + ConsumerConnection connection = new ConsumerConnection(); + connection.setMessageModel(MessageModel.BROADCASTING); + connection.setConsumeType(ConsumeType.CONSUME_PASSIVELY); + HashSet connections = new HashSet<>(); + connections.add(new Connection()); + connection.setConnectionSet(connections); + when(mQClientAPIImpl.getConsumerConnectionList(anyString(), anyString(), anyLong())).thenReturn(connection); + ConsumeStats consumeStats = new ConsumeStats(); + when(mQClientAPIImpl.getConsumeStats(anyString(), anyString(), (String) isNull(), anyLong())).thenReturn(consumeStats); + List broadcastMessageTracks = defaultMQAdminExt.messageTrackDetail(messageExt); + assertThat(broadcastMessageTracks.size()).isEqualTo(2); + assertThat(broadcastMessageTracks.get(0).getTrackType()).isEqualTo(TrackType.CONSUME_BROADCASTING); + } + + @Test + public void testGetConsumeStatus() throws InterruptedException, RemotingException, MQClientException, MQBrokerException { + Map> result = defaultMQAdminExt.getConsumeStatus("unit-test", "default-broker-group", "127.0.0.1:10911"); + assertThat(result.size()).isEqualTo(0); + } + + @Test + public void testGetTopicClusterList() throws InterruptedException, RemotingException, MQClientException, MQBrokerException { + Set result = defaultMQAdminExt.getTopicClusterList("unit-test"); + assertThat(result.size()).isEqualTo(0); + } + + @Test + public void testGetClusterList() throws InterruptedException, RemotingTimeoutException, MQClientException, RemotingSendRequestException, RemotingConnectException { + Set clusterlist = defaultMQAdminExt.getClusterList("UnitTest"); + assertThat(clusterlist.contains("default-cluster-one")).isTrue(); + assertThat(clusterlist.contains("default-cluster-two")).isTrue(); + } + + @Test + public void testFetchConsumeStatsInBroker() throws InterruptedException, RemotingTimeoutException, MQClientException, RemotingSendRequestException, RemotingConnectException { + ConsumeStatsList result = new ConsumeStatsList(); + result.setBrokerAddr("127.0.0.1:10911"); + when(mqClientInstance.getMQClientAPIImpl().fetchConsumeStatsInBroker("127.0.0.1:10911", false, 10000)).thenReturn(result); + ConsumeStatsList consumeStatsList = defaultMQAdminExt.fetchConsumeStatsInBroker("127.0.0.1:10911", false, 10000); + assertThat(consumeStatsList.getBrokerAddr()).isEqualTo("127.0.0.1:10911"); + } + + @Test + public void testGetAllSubscriptionGroup() throws InterruptedException, MQBrokerException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException { + SubscriptionGroupWrapper subscriptionGroupWrapper = defaultMQAdminExt.getAllSubscriptionGroup("127.0.0.1:10911", 10000); + assertThat(subscriptionGroupWrapper.getSubscriptionGroupTable().get("Consumer-group-one").getBrokerId()).isEqualTo(1234); + assertThat(subscriptionGroupWrapper.getSubscriptionGroupTable().get("Consumer-group-one").getGroupName()).isEqualTo("Consumer-group-one"); + assertThat(subscriptionGroupWrapper.getSubscriptionGroupTable().get("Consumer-group-one").isConsumeBroadcastEnable()).isTrue(); + } + + @Test + @Ignore + public void testMaxOffset() throws Exception { + when(mQClientAPIImpl.getMaxOffset(anyString(), any(MessageQueue.class), anyLong())).thenReturn(100L); + assertThat(defaultMQAdminExt.maxOffset(new MessageQueue(TOPIC1, BROKER1_NAME, 0))).isEqualTo(100L); + } + + @Test + @Ignore + public void testSearchOffset() throws Exception { + when(mQClientAPIImpl.searchOffset(anyString(), any(MessageQueue.class), anyLong(), anyLong())).thenReturn(101L); + assertThat(defaultMQAdminExt.searchOffset(new MessageQueue(TOPIC1, BROKER1_NAME, 0), System.currentTimeMillis())).isEqualTo(101L); + } + + @Test + public void testSearchOffsetWithSpecificBoundaryType() throws Exception { + // do mock + DefaultMQAdminExt mockDefaultMQAdminExt = mock(DefaultMQAdminExt.class); + when(mockDefaultMQAdminExt.minOffset(any(MessageQueue.class))).thenReturn(0L); + when(mockDefaultMQAdminExt.maxOffset(any(MessageQueue.class))).thenReturn(101L); + when(mockDefaultMQAdminExt.searchLowerBoundaryOffset(any(MessageQueue.class), anyLong())).thenReturn(0L); + when(mockDefaultMQAdminExt.searchUpperBoundaryOffset(any(MessageQueue.class), anyLong())).thenReturn(100L); + when(mockDefaultMQAdminExt.queryConsumeTimeSpan(anyString(), anyString())).thenReturn(mockQueryConsumeTimeSpan()); + + for (QueueTimeSpan timeSpan: mockDefaultMQAdminExt.queryConsumeTimeSpan(TOPIC1, "group_one")) { + MessageQueue mq = timeSpan.getMessageQueue(); + long maxOffset = mockDefaultMQAdminExt.maxOffset(mq); + long minOffset = mockDefaultMQAdminExt.minOffset(mq); + // if there is at least one message in queue, the maxOffset returns the queue's latest offset + 1 + assertThat((maxOffset == 0 ? 0 : maxOffset - 1) == mockDefaultMQAdminExt.searchUpperBoundaryOffset(mq, timeSpan.getMaxTimeStamp())).isTrue(); + assertThat(minOffset == mockDefaultMQAdminExt.searchLowerBoundaryOffset(mq, timeSpan.getMinTimeStamp())).isTrue(); + } + } + + private List mockQueryConsumeTimeSpan() { + List spanSet = new ArrayList<>(); + QueueTimeSpan timeSpan = new QueueTimeSpan(); + timeSpan.setMessageQueue(new MessageQueue(TOPIC1, BROKER1_NAME, 0)); + timeSpan.setMinTimeStamp(1690421253000L); + timeSpan.setMaxTimeStamp(1690507653000L); + spanSet.add(timeSpan); + return spanSet; + } + + @Test + public void testExamineTopicConfig() throws MQBrokerException, RemotingException, InterruptedException { + TopicConfig topicConfig = defaultMQAdminExt.examineTopicConfig("127.0.0.1:10911", "topic_test_examine_topicConfig"); + assertThat(topicConfig.getTopicName().equals("topic_test_examine_topicConfig")).isTrue(); + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/CommandUtilTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/CommandUtilTest.java new file mode 100644 index 0000000..ea08935 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/CommandUtilTest.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.tools.command; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +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.impl.MQClientAPIImpl; +import org.apache.rocketmq.client.impl.MQClientManager; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +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.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExtImpl; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class CommandUtilTest { + private DefaultMQAdminExt defaultMQAdminExt; + private DefaultMQAdminExtImpl defaultMQAdminExtImpl; + private MQClientInstance mqClientInstance = MQClientManager.getInstance().getOrCreateMQClientInstance(new ClientConfig()); + private MQClientAPIImpl mQClientAPIImpl; + + @Before + public void setup() throws MQClientException, NoSuchFieldException, IllegalAccessException, InterruptedException, MQBrokerException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException { + defaultMQAdminExt = mock(DefaultMQAdminExt.class); + MQClientAPIImpl mQClientAPIImpl = mock(MQClientAPIImpl.class); + defaultMQAdminExtImpl = new DefaultMQAdminExtImpl(defaultMQAdminExt, 3000); + + Field field = DefaultMQAdminExtImpl.class.getDeclaredField("mqClientInstance"); + field.setAccessible(true); + field.set(defaultMQAdminExtImpl, mqClientInstance); + field = MQClientInstance.class.getDeclaredField("mQClientAPIImpl"); + field.setAccessible(true); + field.set(mqClientInstance, mQClientAPIImpl); + + ClusterInfo clusterInfo = new ClusterInfo(); + HashMap brokerAddrTable = new HashMap<>(); + HashMap> clusterAddrTable = new HashMap<>(); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(1234L, "127.0.0.1:10911"); + BrokerData brokerData = new BrokerData(); + brokerData.setBrokerName("default-broker"); + brokerData.setCluster("default-cluster"); + brokerData.setBrokerAddrs(brokerAddrs); + brokerAddrTable.put("default-broker", brokerData); + brokerAddrTable.put("broker-test", new BrokerData()); + Set brokerSet = new HashSet<>(); + brokerSet.add("default-broker"); + brokerSet.add("default-broker-one"); + clusterAddrTable.put("default-cluster", brokerSet); + clusterInfo.setBrokerAddrTable(brokerAddrTable); + clusterInfo.setClusterAddrTable(clusterAddrTable); + when(mQClientAPIImpl.getBrokerClusterInfo(anyLong())).thenReturn(clusterInfo); + when(mQClientAPIImpl.cleanExpiredConsumeQueue(anyString(), anyLong())).thenReturn(true); + } + + @After + public void shutdown() throws Exception { + } + + @Test + public void testFetchMasterAndSlaveDistinguish() throws InterruptedException, MQBrokerException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException { + Map> result = CommandUtil.fetchMasterAndSlaveDistinguish(defaultMQAdminExtImpl, "default-cluster"); + assertThat(result.get(CommandUtil.NO_MASTER_PLACEHOLDER).get(0)).isEqualTo("127.0.0.1:10911"); + } + + @Test + public void testFetchMasterAddrByClusterName() throws InterruptedException, MQBrokerException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException { + Set result = CommandUtil.fetchMasterAddrByClusterName(defaultMQAdminExtImpl, "default-cluster"); + assertThat(result.size()).isEqualTo(0); + } + + @Test + public void testFetchBrokerNameByClusterName() throws Exception { + Set result = CommandUtil.fetchBrokerNameByClusterName(defaultMQAdminExtImpl, "default-cluster"); + assertThat(result.contains("default-broker")).isTrue(); + assertThat(result.contains("default-broker-one")).isTrue(); + assertThat(result.size()).isEqualTo(2); + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/broker/BrokerConsumeStatsSubCommadTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/broker/BrokerConsumeStatsSubCommadTest.java new file mode 100644 index 0000000..d7315d0 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/broker/BrokerConsumeStatsSubCommadTest.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.tools.command.broker; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +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.remoting.exception.RemotingConnectException; +import org.apache.rocketmq.remoting.exception.RemotingSendRequestException; +import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; +import org.apache.rocketmq.remoting.protocol.body.ConsumeStatsList; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExtImpl; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class BrokerConsumeStatsSubCommadTest { + + private static BrokerConsumeStatsSubCommad cmd = new BrokerConsumeStatsSubCommad(); + + private static DefaultMQAdminExt defaultMQAdminExt; + private static DefaultMQAdminExtImpl defaultMQAdminExtImpl; + private static MQClientInstance mqClientInstance = MQClientManager.getInstance().getOrCreateMQClientInstance(new ClientConfig()); + private static MQClientAPIImpl mQClientAPIImpl; + + @BeforeClass + public static void init() throws NoSuchFieldException, IllegalAccessException, InterruptedException, RemotingTimeoutException, MQClientException, RemotingSendRequestException, RemotingConnectException { + mQClientAPIImpl = mock(MQClientAPIImpl.class); + defaultMQAdminExt = new DefaultMQAdminExt(); + defaultMQAdminExtImpl = new DefaultMQAdminExtImpl(defaultMQAdminExt, 1000); + + Field field = DefaultMQAdminExtImpl.class.getDeclaredField("mqClientInstance"); + field.setAccessible(true); + field.set(defaultMQAdminExtImpl, mqClientInstance); + field = MQClientInstance.class.getDeclaredField("mQClientAPIImpl"); + field.setAccessible(true); + field.set(mqClientInstance, mQClientAPIImpl); + field = DefaultMQAdminExt.class.getDeclaredField("defaultMQAdminExtImpl"); + field.setAccessible(true); + field.set(defaultMQAdminExt, defaultMQAdminExtImpl); + + ConsumeStatsList consumeStatsList = new ConsumeStatsList(); + consumeStatsList.setBrokerAddr("127.0l.0.1:10911"); + consumeStatsList.setConsumeStatsList(new ArrayList<>()); + consumeStatsList.setTotalDiff(123); + when(mQClientAPIImpl.fetchConsumeStatsInBroker(anyString(), anyBoolean(), anyLong())).thenReturn(consumeStatsList); + } + + @AfterClass + public static void terminate() { + } + + @Test + public void testExecute() throws SubCommandException, IllegalAccessException, NoSuchFieldException { + + Field field = BrokerConsumeStatsSubCommad.class.getDeclaredField("defaultMQAdminExt"); + field.setAccessible(true); + field.set(cmd, defaultMQAdminExt); + + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-b 127.0.0.1:10911", "-t 3000", "-l 5", "-o true"}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/broker/BrokerStatusSubCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/broker/BrokerStatusSubCommandTest.java new file mode 100644 index 0000000..c685a06 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/broker/BrokerStatusSubCommandTest.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.tools.command.broker; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.protocol.body.BrokerStatsData; +import org.apache.rocketmq.remoting.protocol.body.BrokerStatsItem; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.apache.rocketmq.tools.command.server.ServerResponseMocker; +import org.junit.Test; + +public class BrokerStatusSubCommandTest extends ServerResponseMocker { + @Override + protected byte[] getBody() { + BrokerStatsData brokerStatsData = new BrokerStatsData(); + BrokerStatsItem item = new BrokerStatsItem(); + brokerStatsData.setStatsDay(item); + return brokerStatsData.encode(); + } + + @Test + public void testExecute() throws SubCommandException { + BrokerStatusSubCommand cmd = new BrokerStatusSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-b 127.0.0.1:" + listenPort()}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + + cmd.execute(commandLine, options, null); + } + +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/broker/CleanExpiredCQSubCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/broker/CleanExpiredCQSubCommandTest.java new file mode 100644 index 0000000..7fc4536 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/broker/CleanExpiredCQSubCommandTest.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.tools.command.broker; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.apache.rocketmq.tools.command.server.ServerResponseMocker; +import org.junit.Test; + +public class CleanExpiredCQSubCommandTest extends ServerResponseMocker { + @Override + protected byte[] getBody() { + return null; + } + + @Test + public void testExecute() throws SubCommandException { + CleanExpiredCQSubCommand cmd = new CleanExpiredCQSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-b 127.0.0.1:" + listenPort(), "-c default-cluster"}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/broker/CleanUnusedTopicCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/broker/CleanUnusedTopicCommandTest.java new file mode 100644 index 0000000..6302818 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/broker/CleanUnusedTopicCommandTest.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.tools.command.broker; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.apache.rocketmq.tools.command.server.ServerResponseMocker; +import org.junit.Test; + +public class CleanUnusedTopicCommandTest extends ServerResponseMocker { + @Override + protected byte[] getBody() { + return null; + } + + @Test + public void testExecute() throws SubCommandException { + CleanUnusedTopicCommand cmd = new CleanUnusedTopicCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-b 127.0.0.1:" + listenPort(), "-c default-cluster"}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/broker/DeleteExpiredCommitLogSubCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/broker/DeleteExpiredCommitLogSubCommandTest.java new file mode 100644 index 0000000..931d2b2 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/broker/DeleteExpiredCommitLogSubCommandTest.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.tools.command.broker; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.apache.rocketmq.tools.command.server.ServerResponseMocker; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class DeleteExpiredCommitLogSubCommandTest extends ServerResponseMocker { + + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final PrintStream originalErr = System.err; + + @Before + public void setUp() throws Exception { + System.setOut(new PrintStream(outContent)); + System.setErr(new PrintStream(errContent)); + } + + @After + public void tearDown() throws Exception { + System.setOut(originalOut); + System.setErr(originalErr); + } + + @Override + protected byte[] getBody() { + return null; + } + + @Test + public void testExecute() throws SubCommandException { + DeleteExpiredCommitLogSubCommand cmd = new DeleteExpiredCommitLogSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-b 127.0.0.1:" + listenPort(), "-c default-cluster"}; + final CommandLine commandLine = ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + Assert.assertTrue(outContent.toString().contains("success")); + Assert.assertEquals("", errContent.toString()); + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/broker/GetBrokerConfigCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/broker/GetBrokerConfigCommandTest.java new file mode 100644 index 0000000..7d76daf --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/broker/GetBrokerConfigCommandTest.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.tools.command.broker; + +import java.io.UnsupportedEncodingException; +import java.util.Properties; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.apache.rocketmq.tools.command.server.ServerResponseMocker; +import org.junit.Test; + +public class GetBrokerConfigCommandTest extends ServerResponseMocker { + + @Override + protected byte[] getBody() { + StringBuilder sb = new StringBuilder(); + Properties properties = new Properties(); + properties.setProperty("stat", "123"); + properties.setProperty("ip", "192.168.1.1"); + properties.setProperty("broker_name", "broker_101"); + sb.append(MixAll.properties2String(properties)); + try { + return sb.toString().getBytes(MixAll.DEFAULT_CHARSET); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + @Test + public void testExecute() throws SubCommandException { + GetBrokerConfigCommand cmd = new GetBrokerConfigCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-b 127.0.0.1:" + listenPort(), "-c default-cluster"}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/broker/SendMsgStatusCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/broker/SendMsgStatusCommandTest.java new file mode 100644 index 0000000..335dd2e --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/broker/SendMsgStatusCommandTest.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.tools.command.broker; + +import java.lang.reflect.Field; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +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.impl.MQClientAPIImpl; +import org.apache.rocketmq.client.impl.MQClientManager; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +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.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExtImpl; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.mockito.Mockito.mock; + +public class SendMsgStatusCommandTest { + private static DefaultMQAdminExt defaultMQAdminExt; + private static DefaultMQAdminExtImpl defaultMQAdminExtImpl; + private static MQClientInstance mqClientInstance = MQClientManager.getInstance().getOrCreateMQClientInstance(new ClientConfig()); + private static MQClientAPIImpl mQClientAPIImpl; + + @BeforeClass + public static void init() throws NoSuchFieldException, IllegalAccessException, InterruptedException, RemotingTimeoutException, MQClientException, RemotingSendRequestException, RemotingConnectException, MQBrokerException { + mQClientAPIImpl = mock(MQClientAPIImpl.class); + defaultMQAdminExt = new DefaultMQAdminExt(); + defaultMQAdminExtImpl = new DefaultMQAdminExtImpl(defaultMQAdminExt, 1000); + + Field field = DefaultMQAdminExtImpl.class.getDeclaredField("mqClientInstance"); + field.setAccessible(true); + field.set(defaultMQAdminExtImpl, mqClientInstance); + field = MQClientInstance.class.getDeclaredField("mQClientAPIImpl"); + field.setAccessible(true); + field.set(mqClientInstance, mQClientAPIImpl); + field = DefaultMQAdminExt.class.getDeclaredField("defaultMQAdminExtImpl"); + field.setAccessible(true); + field.set(defaultMQAdminExt, defaultMQAdminExtImpl); + } + + @AfterClass + public static void terminate() { + defaultMQAdminExt.shutdown(); + } + + @Test + public void testExecute() { + SendMsgStatusCommand cmd = new SendMsgStatusCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-b 127.0.0.1:10911", "-s 1024 -c 10"}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + //cmd.execute(commandLine, options, null); + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/broker/UpdateBrokerConfigSubCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/broker/UpdateBrokerConfigSubCommandTest.java new file mode 100644 index 0000000..d25cf19 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/broker/UpdateBrokerConfigSubCommandTest.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.tools.command.broker; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.apache.rocketmq.tools.command.server.ServerResponseMocker; +import org.junit.Test; + +public class UpdateBrokerConfigSubCommandTest extends ServerResponseMocker { + + @Override + protected byte[] getBody() { + return null; + } + + @Test + public void testExecute() throws SubCommandException { + UpdateBrokerConfigSubCommand cmd = new UpdateBrokerConfigSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-b 127.0.0.1:" + listenPort(), "-c default-cluster", "-k topicname", "-v unit_test"}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/connection/ConsumerConnectionSubCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/connection/ConsumerConnectionSubCommandTest.java new file mode 100644 index 0000000..f5967f5 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/connection/ConsumerConnectionSubCommandTest.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.tools.command.connection; + +import java.util.HashSet; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.protocol.body.Connection; +import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.apache.rocketmq.tools.command.server.NameServerMocker; +import org.apache.rocketmq.tools.command.server.ServerResponseMocker; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.Mockito.mock; + +public class ConsumerConnectionSubCommandTest { + private ServerResponseMocker brokerMocker; + + private ServerResponseMocker nameServerMocker; + + @Before + public void before() { + brokerMocker = startOneBroker(); + nameServerMocker = NameServerMocker.startByDefaultConf(brokerMocker.listenPort()); + } + + @After + public void after() { + brokerMocker.shutdown(); + nameServerMocker.shutdown(); + } + + @Test + public void testExecute() throws SubCommandException { + ConsumerConnectionSubCommand cmd = new ConsumerConnectionSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-g default-consumer-group", "-b localhost:" + brokerMocker.listenPort()}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + } + + private ServerResponseMocker startOneBroker() { + ConsumerConnection consumerConnection = new ConsumerConnection(); + HashSet connectionSet = new HashSet<>(); + Connection connection = mock(Connection.class); + connectionSet.add(connection); + consumerConnection.setConnectionSet(connectionSet); + // start broker + return ServerResponseMocker.startServer(consumerConnection.encode()); + } + +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/connection/ProducerConnectionSubCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/connection/ProducerConnectionSubCommandTest.java new file mode 100644 index 0000000..672e411 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/connection/ProducerConnectionSubCommandTest.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.tools.command.connection; + +import java.util.HashSet; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.protocol.body.Connection; +import org.apache.rocketmq.remoting.protocol.body.ProducerConnection; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.apache.rocketmq.tools.command.server.NameServerMocker; +import org.apache.rocketmq.tools.command.server.ServerResponseMocker; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.Mockito.mock; + +public class ProducerConnectionSubCommandTest { + + private ServerResponseMocker brokerMocker; + + private ServerResponseMocker nameServerMocker; + + @Before + public void before() { + brokerMocker = startOneBroker(); + nameServerMocker = NameServerMocker.startByDefaultConf(brokerMocker.listenPort()); + } + + @After + public void after() { + brokerMocker.shutdown(); + nameServerMocker.shutdown(); + } + + @Test + public void testExecute() throws SubCommandException { + ProducerConnectionSubCommand cmd = new ProducerConnectionSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-g default-producer-group", "-t unit-test", String.format("-n localhost:%d", nameServerMocker.listenPort())}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + } + + private ServerResponseMocker startOneBroker() { + ProducerConnection producerConnection = new ProducerConnection(); + HashSet connectionSet = new HashSet<>(); + Connection connection = mock(Connection.class); + connectionSet.add(connection); + producerConnection.setConnectionSet(connectionSet); + + // start broker + return ServerResponseMocker.startServer(producerConnection.encode()); + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/consumer/ConsumerProgressSubCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/consumer/ConsumerProgressSubCommandTest.java new file mode 100644 index 0000000..15c3fa7 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/consumer/ConsumerProgressSubCommandTest.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.tools.command.consumer; + +import java.util.HashMap; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.admin.ConsumeStats; +import org.apache.rocketmq.remoting.protocol.admin.OffsetWrapper; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.apache.rocketmq.tools.command.server.NameServerMocker; +import org.apache.rocketmq.tools.command.server.ServerResponseMocker; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +public class ConsumerProgressSubCommandTest { + + private ServerResponseMocker brokerMocker; + + private ServerResponseMocker nameServerMocker; + + @Before + public void before() { + brokerMocker = startOneBroker(); + nameServerMocker = NameServerMocker.startByDefaultConf(brokerMocker.listenPort()); + } + + @After + public void after() { + brokerMocker.shutdown(); + nameServerMocker.shutdown(); + } + + @Ignore + @Test + public void testExecute() throws SubCommandException { + ConsumerProgressSubCommand cmd = new ConsumerProgressSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-g default-group", + String.format("-n localhost:%d", nameServerMocker.listenPort())}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + } + + private ServerResponseMocker startOneBroker() { + ConsumeStats consumeStats = new ConsumeStats(); + HashMap offsetTable = new HashMap<>(); + MessageQueue messageQueue = new MessageQueue(); + messageQueue.setBrokerName("mockBrokerName"); + messageQueue.setQueueId(1); + messageQueue.setBrokerName("mockTopicName"); + + OffsetWrapper offsetWrapper = new OffsetWrapper(); + offsetWrapper.setBrokerOffset(1); + offsetWrapper.setConsumerOffset(1); + offsetWrapper.setLastTimestamp(System.currentTimeMillis()); + + offsetTable.put(messageQueue, offsetWrapper); + consumeStats.setOffsetTable(offsetTable); + // start broker + return ServerResponseMocker.startServer(consumeStats.encode()); + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/consumer/ConsumerStatusSubCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/consumer/ConsumerStatusSubCommandTest.java new file mode 100644 index 0000000..4651113 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/consumer/ConsumerStatusSubCommandTest.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.tools.command.consumer; + +import java.util.HashSet; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.protocol.body.Connection; +import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.apache.rocketmq.tools.command.server.NameServerMocker; +import org.apache.rocketmq.tools.command.server.ServerResponseMocker; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.Mockito.mock; + +public class ConsumerStatusSubCommandTest { + + private ServerResponseMocker brokerMocker; + + private ServerResponseMocker nameServerMocker; + + @Before + public void before() { + brokerMocker = startOneBroker(); + nameServerMocker = NameServerMocker.startByDefaultConf(brokerMocker.listenPort()); + } + + @After + public void after() { + brokerMocker.shutdown(); + nameServerMocker.shutdown(); + } + + @Test + public void testExecute() throws SubCommandException { + ConsumerStatusSubCommand cmd = new ConsumerStatusSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-g default-group", "-i cid_one", + String.format("-n localhost:%d", nameServerMocker.listenPort())}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + } + + private ServerResponseMocker startOneBroker() { + ConsumerConnection consumerConnection = new ConsumerConnection(); + HashSet connectionSet = new HashSet<>(); + Connection connection = mock(Connection.class); + connectionSet.add(connection); + consumerConnection.setConnectionSet(connectionSet); + // start broker + return ServerResponseMocker.startServer(consumerConnection.encode()); + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/consumer/GetConsumerConfigSubCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/consumer/GetConsumerConfigSubCommandTest.java new file mode 100644 index 0000000..e4e5e97 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/consumer/GetConsumerConfigSubCommandTest.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.tools.command.consumer; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.body.Connection; +import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.apache.rocketmq.tools.command.server.ServerResponseMocker; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.Mockito.mock; + +public class GetConsumerConfigSubCommandTest { + + private ServerResponseMocker brokerMocker; + + private ServerResponseMocker nameServerMocker; + + @Before + public void before() { + brokerMocker = startOneBroker(); + nameServerMocker = startNameServer(); + } + + @After + public void after() { + brokerMocker.shutdown(); + nameServerMocker.shutdown(); + } + + @Test + public void testExecute() throws SubCommandException { + GetConsumerConfigSubCommand cmd = new GetConsumerConfigSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-g group_test", String.format("-n localhost:%d", nameServerMocker.listenPort())}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), + new DefaultParser()); + cmd.execute(commandLine, options, null); + } + + private ServerResponseMocker startNameServer() { + ClusterInfo clusterInfo = new ClusterInfo(); + + HashMap brokerAddressTable = new HashMap<>(); + BrokerData brokerData = new BrokerData(); + brokerData.setBrokerName("mockBrokerName"); + HashMap brokerAddress = new HashMap<>(); + brokerAddress.put(1L, "127.0.0.1:" + brokerMocker.listenPort()); + brokerData.setBrokerAddrs(brokerAddress); + brokerData.setCluster("mockCluster"); + brokerAddressTable.put("mockBrokerName", brokerData); + clusterInfo.setBrokerAddrTable(brokerAddressTable); + + HashMap> clusterAddressTable = new HashMap<>(); + Set brokerNames = new HashSet<>(); + brokerNames.add("mockBrokerName"); + clusterAddressTable.put("mockCluster", brokerNames); + clusterInfo.setClusterAddrTable(clusterAddressTable); + + // start name server + return ServerResponseMocker.startServer(clusterInfo.encode()); + } + + private ServerResponseMocker startOneBroker() { + ConsumerConnection consumerConnection = new ConsumerConnection(); + HashSet connectionSet = new HashSet<>(); + Connection connection = mock(Connection.class); + connectionSet.add(connection); + consumerConnection.setConnectionSet(connectionSet); + // start broker + return ServerResponseMocker.startServer(consumerConnection.encode()); + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/consumer/UpdateSubGroupListSubCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/consumer/UpdateSubGroupListSubCommandTest.java new file mode 100644 index 0000000..0c23787 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/consumer/UpdateSubGroupListSubCommandTest.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.tools.command.consumer; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class UpdateSubGroupListSubCommandTest { + + @Test + public void testArguments() { + UpdateSubGroupListSubCommand cmd = new UpdateSubGroupListSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + + String brokerAddress = "127.0.0.1:10911"; + String inputFileName = "groups.json"; + String[] args = new String[] {"-b " + brokerAddress, "-f " + inputFileName}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), args, + cmd.buildCommandlineOptions(options), new DefaultParser()); + + assertEquals(brokerAddress, commandLine.getOptionValue('b').trim()); + assertEquals(inputFileName, commandLine.getOptionValue('f').trim()); + } +} \ No newline at end of file diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/message/ConsumeMessageCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/message/ConsumeMessageCommandTest.java new file mode 100644 index 0000000..f6c3b7c --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/message/ConsumeMessageCommandTest.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.tools.command.message; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +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.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +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.mock; +import static org.mockito.Mockito.when; + +public class ConsumeMessageCommandTest { + private static ConsumeMessageCommand consumeMessageCommand; + + private static final PullResult PULL_RESULT = mockPullResult(); + + private static PullResult mockPullResult() { + MessageExt msg = new MessageExt(); + msg.setBody(new byte[] {'a'}); + List msgFoundList = new ArrayList<>(); + msgFoundList.add(msg); + return new PullResult(PullStatus.FOUND, 2, 0, 1, msgFoundList); + } + + + @BeforeClass + public static void init() throws MQClientException, RemotingException, MQBrokerException, InterruptedException, + NoSuchFieldException, IllegalAccessException { + consumeMessageCommand = new ConsumeMessageCommand(); + DefaultMQPullConsumer defaultMQPullConsumer = mock(DefaultMQPullConsumer.class); + + assignPullResult(defaultMQPullConsumer); + when(defaultMQPullConsumer.minOffset(any(MessageQueue.class))).thenReturn(Long.valueOf(0)); + when(defaultMQPullConsumer.maxOffset(any(MessageQueue.class))).thenReturn(Long.valueOf(1)); + + final Set mqList = new HashSet<>(); + mqList.add(new MessageQueue()); + when(defaultMQPullConsumer.fetchSubscribeMessageQueues(anyString())).thenReturn(mqList); + + Field producerField = ConsumeMessageCommand.class.getDeclaredField("defaultMQPullConsumer"); + producerField.setAccessible(true); + producerField.set(consumeMessageCommand, defaultMQPullConsumer); + } + + @AfterClass + public static void terminate() { + } + + private static void assignPullResult() { + assignPullResult(null); + } + + private static void assignPullResult(DefaultMQPullConsumer defaultMQPullConsumer) { + try { + if (defaultMQPullConsumer == null) { + Field producerField = ConsumeMessageCommand.class.getDeclaredField("defaultMQPullConsumer"); + producerField.setAccessible(true); + defaultMQPullConsumer = (DefaultMQPullConsumer) producerField.get(consumeMessageCommand); + } + when(defaultMQPullConsumer.pull(any(MessageQueue.class), anyString(), anyLong(), anyInt())) + .thenReturn(PULL_RESULT); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + @Test + public void testExecuteDefault() throws SubCommandException { + PrintStream out = System.out; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + System.setOut(new PrintStream(bos)); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-t mytopic", "-n localhost:9876"}; + assignPullResult(); + CommandLine commandLine = ServerUtil.parseCmdLine("mqadmin " + consumeMessageCommand.commandName(), + subargs, consumeMessageCommand.buildCommandlineOptions(options), new DefaultParser()); + consumeMessageCommand.execute(commandLine, options, null); + + System.setOut(out); + String s = new String(bos.toByteArray(), StandardCharsets.UTF_8); + Assert.assertTrue(s.contains("Consume ok")); + } + + @Test + public void testExecuteByCondition() throws SubCommandException { + PrintStream out = System.out; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + System.setOut(new PrintStream(bos)); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + + String[] subargs = new String[] {"-t mytopic", "-b localhost", "-i 0", "-n localhost:9876"}; + CommandLine commandLine = ServerUtil.parseCmdLine("mqadmin " + consumeMessageCommand.commandName(), + subargs, consumeMessageCommand.buildCommandlineOptions(options), new DefaultParser()); + assignPullResult(); + consumeMessageCommand.execute(commandLine, options, null); + System.setOut(out); + String s = new String(bos.toByteArray(), StandardCharsets.UTF_8); + Assert.assertTrue(s.contains("Consume ok")); + } + + @Test + public void testExecuteDefaultWhenPullMessageByQueueGotException() throws SubCommandException, InterruptedException, RemotingException, MQClientException, MQBrokerException, NoSuchFieldException, IllegalAccessException { + DefaultMQPullConsumer defaultMQPullConsumer = mock(DefaultMQPullConsumer.class); + when(defaultMQPullConsumer.pull(any(MessageQueue.class), anyString(), anyLong(), anyInt())).thenThrow(MQClientException.class); + Field producerField = ConsumeMessageCommand.class.getDeclaredField("defaultMQPullConsumer"); + producerField.setAccessible(true); + producerField.set(consumeMessageCommand, defaultMQPullConsumer); + + PrintStream out = System.out; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + System.setOut(new PrintStream(bos)); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-t topic-not-existu", "-n localhost:9876"}; + CommandLine commandLine = ServerUtil.parseCmdLine("mqadmin " + consumeMessageCommand.commandName(), + subargs, consumeMessageCommand.buildCommandlineOptions(options), new DefaultParser()); + consumeMessageCommand.execute(commandLine, options, null); + + System.setOut(out); + String s = new String(bos.toByteArray(), StandardCharsets.UTF_8); + Assert.assertFalse(s.contains("Consume ok")); + } + + @Test + public void testExecuteByConditionWhenPullMessageByQueueGotException() throws IllegalAccessException, InterruptedException, RemotingException, MQClientException, MQBrokerException, NoSuchFieldException, SubCommandException { + DefaultMQPullConsumer defaultMQPullConsumer = mock(DefaultMQPullConsumer.class); + when(defaultMQPullConsumer.pull(any(MessageQueue.class), anyString(), anyLong(), anyInt())).thenThrow(MQClientException.class); + Field producerField = ConsumeMessageCommand.class.getDeclaredField("defaultMQPullConsumer"); + producerField.setAccessible(true); + producerField.set(consumeMessageCommand, defaultMQPullConsumer); + + PrintStream out = System.out; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + System.setOut(new PrintStream(bos)); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + + String[] subargs = new String[] {"-t mytopic", "-b localhost", "-i 0", "-n localhost:9876"}; + CommandLine commandLine = ServerUtil.parseCmdLine("mqadmin " + consumeMessageCommand.commandName(), + subargs, consumeMessageCommand.buildCommandlineOptions(options), new DefaultParser()); + consumeMessageCommand.execute(commandLine, options, null); + + System.setOut(out); + String s = new String(bos.toByteArray(), StandardCharsets.UTF_8); + Assert.assertFalse(s.contains("Consume ok")); + } +} \ No newline at end of file diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/message/QueryMsgByUniqueKeySubCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/message/QueryMsgByUniqueKeySubCommandTest.java new file mode 100644 index 0000000..b24bd22 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/message/QueryMsgByUniqueKeySubCommandTest.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.tools.command.message; + +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.QueryResult; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +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.factory.MQClientInstance; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +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.LanguageCode; +import org.apache.rocketmq.remoting.protocol.admin.ConsumeStats; +import org.apache.rocketmq.remoting.protocol.admin.OffsetWrapper; +import org.apache.rocketmq.remoting.protocol.body.CMResult; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.body.Connection; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection; +import org.apache.rocketmq.remoting.protocol.body.GroupList; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExtImpl; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.assertj.core.util.Lists; +import org.junit.Before; +import org.junit.Test; + +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.mock; +import static org.mockito.Mockito.when; + +public class QueryMsgByUniqueKeySubCommandTest { + + private static QueryMsgByUniqueKeySubCommand cmd = new QueryMsgByUniqueKeySubCommand(); + + private static DefaultMQAdminExt defaultMQAdminExt; + private static DefaultMQAdminExtImpl defaultMQAdminExtImpl; + private static MQClientInstance mqClientInstance = MQClientManager.getInstance().getOrCreateMQClientInstance(new ClientConfig()); + + private static MQClientAPIImpl mQClientAPIImpl; + private static MQAdminImpl mQAdminImpl; + + @Before + public void before() throws NoSuchFieldException, IllegalAccessException, InterruptedException, RemotingException, MQClientException, MQBrokerException { + + mQClientAPIImpl = mock(MQClientAPIImpl.class); + mQAdminImpl = mock(MQAdminImpl.class); + + defaultMQAdminExt = new DefaultMQAdminExt(); + defaultMQAdminExtImpl = new DefaultMQAdminExtImpl(defaultMQAdminExt, 1000); + + Field field = DefaultMQAdminExtImpl.class.getDeclaredField("mqClientInstance"); + field.setAccessible(true); + field.set(defaultMQAdminExtImpl, mqClientInstance); + + field = MQClientInstance.class.getDeclaredField("mQClientAPIImpl"); + field.setAccessible(true); + field.set(mqClientInstance, mQClientAPIImpl); + + field = MQClientInstance.class.getDeclaredField("mQAdminImpl"); + field.setAccessible(true); + field.set(mqClientInstance, mQAdminImpl); + + field = DefaultMQAdminExt.class.getDeclaredField("defaultMQAdminExtImpl"); + field.setAccessible(true); + field.set(defaultMQAdminExt, defaultMQAdminExtImpl); + + ConsumeMessageDirectlyResult result = new ConsumeMessageDirectlyResult(); + result.setConsumeResult(CMResult.CR_SUCCESS); + result.setRemark("customRemark_122333444"); + when(mQClientAPIImpl.consumeMessageDirectly(anyString(), anyString(), anyString(), anyString(), anyString(), anyLong())).thenReturn(result); + + MessageExt retMsgExt = new MessageExt(); + retMsgExt.setMsgId("0A3A54F7BF7D18B4AAC28A3FA2CF0000"); + retMsgExt.setBody("this is message ext body".getBytes()); + retMsgExt.setTopic("testTopic"); + retMsgExt.setTags("testTags"); + retMsgExt.setStoreHost(new InetSocketAddress("127.0.0.1", 8899)); + retMsgExt.setBornHost(new InetSocketAddress("127.0.0.1", 7788)); + retMsgExt.setQueueId(1); + retMsgExt.setQueueOffset(12L); + retMsgExt.setCommitLogOffset(123); + retMsgExt.setReconsumeTimes(2); + retMsgExt.setBornTimestamp(System.currentTimeMillis()); + retMsgExt.setStoreTimestamp(System.currentTimeMillis()); + when(mQAdminImpl.viewMessage(anyString(), anyString())).thenReturn(retMsgExt); + + when(mQAdminImpl.queryMessageByUniqKey(anyString(), anyString())).thenReturn(retMsgExt); + + QueryResult queryResult = new QueryResult(0, Lists.newArrayList(retMsgExt)); + when(mQAdminImpl.queryMessageByUniqKey(anyString(), anyString(), anyString(), anyInt(), anyLong(), anyLong())).thenReturn(queryResult); + + TopicRouteData topicRouteData = new TopicRouteData(); + List brokerDataList = new ArrayList<>(); + BrokerData brokerData = new BrokerData(); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(MixAll.MASTER_ID, "127.0.0.1:9876"); + brokerData.setBrokerAddrs(brokerAddrs); + brokerDataList.add(brokerData); + topicRouteData.setBrokerDatas(brokerDataList); + when(mQClientAPIImpl.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(topicRouteData); + + GroupList groupList = new GroupList(); + HashSet groupSets = new HashSet<>(); + groupSets.add("testGroup"); + groupList.setGroupList(groupSets); + when(mQClientAPIImpl.queryTopicConsumeByWho(anyString(), anyString(), anyLong())).thenReturn(groupList); + + ConsumeStats consumeStats = new ConsumeStats(); + consumeStats.setConsumeTps(100 * 10000); + HashMap offsetTable = new HashMap<>(); + MessageQueue messageQueue = new MessageQueue(); + messageQueue.setBrokerName("messageQueue BrokerName testing"); + messageQueue.setTopic("messageQueue topic"); + messageQueue.setQueueId(1); + OffsetWrapper offsetWrapper = new OffsetWrapper(); + offsetWrapper.setBrokerOffset(100); + offsetWrapper.setConsumerOffset(200); + offsetWrapper.setLastTimestamp(System.currentTimeMillis()); + offsetTable.put(messageQueue, offsetWrapper); + consumeStats.setOffsetTable(offsetTable); + when(mQClientAPIImpl.getConsumeStats(anyString(), anyString(), (String) isNull(), anyLong())).thenReturn(consumeStats); + + ClusterInfo clusterInfo = new ClusterInfo(); + HashMap brokerAddrTable = new HashMap<>(); + brokerAddrTable.put("key", brokerData); + clusterInfo.setBrokerAddrTable(brokerAddrTable); + HashMap> clusterAddrTable = new HashMap<>(); + Set addrSet = new HashSet<>(); + addrSet.add("127.0.0.1:9876"); + clusterAddrTable.put("key", addrSet); + clusterInfo.setClusterAddrTable(clusterAddrTable); + when(mQClientAPIImpl.getBrokerClusterInfo(anyLong())).thenReturn(clusterInfo); + + field = QueryMsgByUniqueKeySubCommand.class.getDeclaredField("defaultMQAdminExt"); + field.setAccessible(true); + field.set(cmd, defaultMQAdminExt); + + } + + @Test + public void testExecuteConsumeActively() throws SubCommandException, InterruptedException, MQBrokerException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException { + + ConsumerConnection consumerConnection = new ConsumerConnection(); + consumerConnection.setConsumeType(ConsumeType.CONSUME_ACTIVELY); + HashSet connectionSet = new HashSet<>(); + Connection conn = new Connection(); + conn.setClientId("clientIdTest"); + conn.setClientAddr("clientAddrTest"); + conn.setLanguage(LanguageCode.JAVA); + conn.setVersion(1); + connectionSet.add(conn); + consumerConnection.setConnectionSet(connectionSet); + when(mQClientAPIImpl.getConsumerConnectionList(anyString(), anyString(), anyLong())).thenReturn(consumerConnection); + + Options options = ServerUtil.buildCommandlineOptions(new Options()); + + String[] args = new String[] {"-t myTopicTest", "-i msgId", "-c DefaultCluster"}; + CommandLine commandLine = ServerUtil.parseCmdLine("mqadmin ", args, + cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + + } + + @Test + public void testExecuteConsumePassively() throws SubCommandException, InterruptedException, MQBrokerException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException { + + ConsumerConnection consumerConnection = new ConsumerConnection(); + consumerConnection.setConsumeType(ConsumeType.CONSUME_PASSIVELY); + HashSet connectionSet = new HashSet<>(); + Connection conn = new Connection(); + conn.setClientId("clientIdTestStr"); + conn.setClientAddr("clientAddrTestStr"); + conn.setLanguage(LanguageCode.JAVA); + conn.setVersion(2); + connectionSet.add(conn); + consumerConnection.setConnectionSet(connectionSet); + when(mQClientAPIImpl.getConsumerConnectionList(anyString(), anyString(), anyLong())).thenReturn(consumerConnection); + + Options options = ServerUtil.buildCommandlineOptions(new Options()); + + String[] args = new String[] {"-t myTopicTest", "-i 7F000001000004D20000000000000066", "-c DefaultCluster"}; + CommandLine commandLine = ServerUtil.parseCmdLine("mqadmin ", args, + cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + + } + + @Test + public void testExecuteWithConsumerGroupAndClientId() throws SubCommandException { + + Options options = ServerUtil.buildCommandlineOptions(new Options()); + + String[] args = new String[] {"-t myTopicTest", "-i 0A3A54F7BF7D18B4AAC28A3FA2CF0000", "-g producerGroupName", "-d clientId", "-c DefaultCluster"}; + CommandLine commandLine = ServerUtil.parseCmdLine("mqadmin ", args, + cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + } + + @Test + public void testExecute() throws SubCommandException { + + System.setProperty("rocketmq.namesrv.addr", "127.0.0.1:9876"); + + String[] args = new String[]{"-t myTopicTest", "-i 0A3A54F7BF7D18B4AAC28A3FA2CF0000", "-c DefaultCluster"}; + Options options = ServerUtil.buildCommandlineOptions(new Options()); + CommandLine commandLine = ServerUtil.parseCmdLine("mqadmin ", args, + cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + + args = new String[] {"-t myTopicTest", "-i 0A3A54F7BF7D18B4AAC28A3FA2CF0000", "-g producerGroupName", "-d clientId", "-c DefaultCluster"}; + commandLine = ServerUtil.parseCmdLine("mqadmin ", args, cmd.buildCommandlineOptions(options), + new DefaultParser()); + cmd.execute(commandLine, options, null); + + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/message/QueryMsgTraceByIdSubCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/message/QueryMsgTraceByIdSubCommandTest.java new file mode 100644 index 0000000..e5be224 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/message/QueryMsgTraceByIdSubCommandTest.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.tools.command.message; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +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.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.apache.rocketmq.tools.command.server.ServerResponseMocker; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class QueryMsgTraceByIdSubCommandTest { + + private ServerResponseMocker brokerMocker; + + private ServerResponseMocker nameServerMocker; + + private static final String MSG_ID = "AC1FF54E81C418B4AAC24F92E1E00000"; + + @Before + public void before() { + brokerMocker = startOneBroker(); + nameServerMocker = startNameServer(); + } + + @After + public void after() { + brokerMocker.shutdown(); + nameServerMocker.shutdown(); + } + + @Test + public void testExecute() throws SubCommandException { + QueryMsgTraceByIdSubCommand cmd = new QueryMsgTraceByIdSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {String.format("-i %s", MSG_ID), + String.format("-n localhost:%d", nameServerMocker.listenPort())}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + } + + private ServerResponseMocker startNameServer() { + TopicRouteData topicRouteData = new TopicRouteData(); + List dataList = new ArrayList<>(); + HashMap brokerAddress = new HashMap<>(); + brokerAddress.put(1L, "127.0.0.1:" + brokerMocker.listenPort()); + BrokerData brokerData = new BrokerData("mockCluster", "mockBrokerName", brokerAddress); + brokerData.setBrokerName("mockBrokerName"); + dataList.add(brokerData); + topicRouteData.setBrokerDatas(dataList); + + List queueDatas = new ArrayList<>(); + QueueData queueData = new QueueData(); + queueData.setBrokerName("mockBrokerName"); + queueData.setPerm(1); + queueData.setReadQueueNums(1); + queueData.setTopicSysFlag(1); + queueData.setWriteQueueNums(1); + queueDatas.add(queueData); + topicRouteData.setQueueDatas(queueDatas); + + return ServerResponseMocker.startServer(topicRouteData.encode()); + } + + private ServerResponseMocker startOneBroker() { + try { + MessageExt messageExt = new MessageExt(); + messageExt.setTopic(TopicValidator.RMQ_SYS_TRACE_TOPIC); + messageExt.setBody(new byte[100]); + // topic RMQ_SYS_TRACE_TOPIC which built-in rocketMQ, set msg id as msg key + messageExt.setKeys(MSG_ID); + messageExt.setBornHost(new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0)); + messageExt.setStoreHost(new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0)); + byte[] body = MessageDecoder.encode(messageExt, false); + + HashMap extMap = new HashMap<>(); + extMap.put("indexLastUpdateTimestamp", String.valueOf(System.currentTimeMillis())); + extMap.put("indexLastUpdatePhyoffset", String.valueOf(System.currentTimeMillis())); + // start broker + return ServerResponseMocker.startServer(body, extMap); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/message/SendMessageCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/message/SendMessageCommandTest.java new file mode 100644 index 0000000..6f750d5 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/message/SendMessageCommandTest.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.tools.command.message; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.Field; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +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.common.message.MessageQueue; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class SendMessageCommandTest { + + private static SendMessageCommand sendMessageCommand = new SendMessageCommand(); + + @BeforeClass + public static void init() throws MQClientException, RemotingException, InterruptedException, MQBrokerException, NoSuchFieldException, IllegalAccessException { + + DefaultMQProducer defaultMQProducer = mock(DefaultMQProducer.class); + SendResult sendResult = new SendResult(); + sendResult.setMessageQueue(new MessageQueue()); + sendResult.getMessageQueue().setBrokerName("broker1"); + sendResult.getMessageQueue().setQueueId(1); + sendResult.setSendStatus(SendStatus.SEND_OK); + sendResult.setMsgId("fgwejigherughwueyutyu4t4343t43"); + + when(defaultMQProducer.send(any(Message.class))).thenReturn(sendResult); + when(defaultMQProducer.send(any(Message.class), any(MessageQueue.class))).thenReturn(sendResult); + + Field producerField = SendMessageCommand.class.getDeclaredField("producer"); + producerField.setAccessible(true); + producerField.set(sendMessageCommand, defaultMQProducer); + } + + @AfterClass + public static void terminate() { + } + + @Test + public void testExecute() throws SubCommandException { + PrintStream out = System.out; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + System.setOut(new PrintStream(bos)); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-t mytopic","-p 'send message test'","-c tagA","-k order-16546745756"}; + CommandLine commandLine = ServerUtil.parseCmdLine("mqadmin " + sendMessageCommand.commandName(), + subargs, sendMessageCommand.buildCommandlineOptions(options), new DefaultParser()); + sendMessageCommand.execute(commandLine, options, null); + + subargs = new String[] {"-t mytopic","-p 'send message test'","-c tagA","-k order-16546745756","-b brokera","-i 1"}; + commandLine = ServerUtil.parseCmdLine("mqadmin " + sendMessageCommand.commandName(), subargs, + sendMessageCommand.buildCommandlineOptions(options), new DefaultParser()); + sendMessageCommand.execute(commandLine, options, null); + System.setOut(out); + String s = new String(bos.toByteArray()); + Assert.assertTrue(s.contains("SEND_OK")); + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/metadata/ExportMetadataInRocksDBCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/metadata/ExportMetadataInRocksDBCommandTest.java new file mode 100644 index 0000000..2b938c9 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/metadata/ExportMetadataInRocksDBCommandTest.java @@ -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. + */ +package org.apache.rocketmq.tools.command.metadata; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.apache.rocketmq.tools.command.export.ExportMetadataInRocksDBCommand; +import org.junit.Test; + +import java.io.File; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ExportMetadataInRocksDBCommandTest { + private static final String BASE_PATH = System.getProperty("user.home") + File.separator + "store/config/"; + + @Test + public void testExecute() throws SubCommandException { + { + String[][] cases = new String[][] { + {"topics", "false"}, + {"topics", "false1"}, + {"topics", "true"}, + {"subscriptionGroups", "false"}, + {"subscriptionGroups", "false2"}, + {"subscriptionGroups", "true"} + }; + + for (String[] c : cases) { + ExportMetadataInRocksDBCommand cmd = new ExportMetadataInRocksDBCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-p " + BASE_PATH + c[0], "-t " + c[0], "-j " + c[1]}; + final CommandLine commandLine = ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + assertThat(commandLine.getOptionValue("p").trim()).isEqualTo(BASE_PATH + c[0]); + assertThat(commandLine.getOptionValue("t").trim()).isEqualTo(c[0]); + assertThat(commandLine.getOptionValue("j").trim()).isEqualTo(c[1]); + } + } + // invalid cases + { + String[][] cases = new String[][] { + {"-p " + BASE_PATH + "tmpPath", "-t topics", "-j true"}, + {"-p ", "-t topics", "-j true"}, + {"-p " + BASE_PATH + "topics", "-t invalid_type", "-j true"} + }; + + for (String[] c : cases) { + ExportMetadataInRocksDBCommand cmd = new ExportMetadataInRocksDBCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + final CommandLine commandLine = ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), c, + cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + } + } + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/namesrv/AddWritePermSubCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/namesrv/AddWritePermSubCommandTest.java new file mode 100644 index 0000000..fdf6c00 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/namesrv/AddWritePermSubCommandTest.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.tools.command.namesrv; + +import java.util.HashMap; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.apache.rocketmq.tools.command.server.NameServerMocker; +import org.apache.rocketmq.tools.command.server.ServerResponseMocker; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class AddWritePermSubCommandTest { + + private ServerResponseMocker brokerMocker; + + private ServerResponseMocker nameServerMocker; + + @Before + public void before() { + brokerMocker = startOneBroker(); + nameServerMocker = startNameServer(); + System.setProperty(MixAll.NAMESRV_ADDR_PROPERTY, "localhost:" + nameServerMocker.listenPort()); + } + + @After + public void after() { + brokerMocker.shutdown(); + nameServerMocker.shutdown(); + } + + @Test + public void testExecute() throws SubCommandException { + AddWritePermSubCommand cmd = new AddWritePermSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[]{"-b default-broker"}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + } + + private ServerResponseMocker startNameServer() { + HashMap extMap = new HashMap<>(); + extMap.put("addTopicCount", "1"); + // start name server + return NameServerMocker.startByDefaultConf(brokerMocker.listenPort(), extMap); + } + + private ServerResponseMocker startOneBroker() { + // start broker + return ServerResponseMocker.startServer(null); + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/namesrv/GetNamesrvConfigCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/namesrv/GetNamesrvConfigCommandTest.java new file mode 100644 index 0000000..c553623 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/namesrv/GetNamesrvConfigCommandTest.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.tools.command.namesrv; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.command.server.NameServerMocker; +import org.apache.rocketmq.tools.command.server.ServerResponseMocker; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class GetNamesrvConfigCommandTest { + + private ServerResponseMocker brokerMocker; + + private ServerResponseMocker nameServerMocker; + + @Before + public void before() { + brokerMocker = startOneBroker(); + nameServerMocker = NameServerMocker.startByDefaultConf(brokerMocker.listenPort()); + } + + @After + public void after() { + brokerMocker.shutdown(); + nameServerMocker.shutdown(); + } + + @Test + public void testExecute() throws Exception { + GetNamesrvConfigCommand cmd = new GetNamesrvConfigCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-n localhost:" + nameServerMocker.listenPort()}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + + cmd.execute(commandLine, options, null); + } + + private ServerResponseMocker startOneBroker() { + // start broker + return ServerResponseMocker.startServer(null); + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/namesrv/UpdateKvConfigCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/namesrv/UpdateKvConfigCommandTest.java new file mode 100644 index 0000000..9cc7b4f --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/namesrv/UpdateKvConfigCommandTest.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.tools.command.namesrv; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.apache.rocketmq.tools.command.server.NameServerMocker; +import org.apache.rocketmq.tools.command.server.ServerResponseMocker; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class UpdateKvConfigCommandTest { + private ServerResponseMocker brokerMocker; + + private ServerResponseMocker nameServerMocker; + + @Before + public void before() { + brokerMocker = startOneBroker(); + nameServerMocker = NameServerMocker.startByDefaultConf(brokerMocker.listenPort()); + } + + @After + public void after() { + brokerMocker.shutdown(); + nameServerMocker.shutdown(); + } + + @Test + public void testExecute() throws SubCommandException { + UpdateKvConfigCommand cmd = new UpdateKvConfigCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] { + "-s namespace", "-k topicname", "-v unit_test", + String.format("-n localhost:%d", nameServerMocker.listenPort())}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName() + cmd.commandDesc(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + } + + private ServerResponseMocker startOneBroker() { + // start broker + return ServerResponseMocker.startServer(null); + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/namesrv/WipeWritePermSubCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/namesrv/WipeWritePermSubCommandTest.java new file mode 100644 index 0000000..b0f9126 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/namesrv/WipeWritePermSubCommandTest.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.tools.command.namesrv; + +import java.util.HashMap; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.apache.rocketmq.tools.command.server.NameServerMocker; +import org.apache.rocketmq.tools.command.server.ServerResponseMocker; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class WipeWritePermSubCommandTest { + private ServerResponseMocker brokerMocker; + + private ServerResponseMocker nameServerMocker; + + @Before + public void before() { + brokerMocker = startOneBroker(); + nameServerMocker = startNameServer(); + } + + @After + public void after() { + brokerMocker.shutdown(); + nameServerMocker.shutdown(); + } + + @Test + public void testExecute() throws SubCommandException { + WipeWritePermSubCommand cmd = new WipeWritePermSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-b default-broker"}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + } + + private ServerResponseMocker startNameServer() { + HashMap extMap = new HashMap<>(); + extMap.put("wipeTopicCount", "1"); + // start name server + return NameServerMocker.startByDefaultConf(brokerMocker.listenPort(), extMap); + } + + private ServerResponseMocker startOneBroker() { + // start broker + HashMap extMap = new HashMap<>(); + extMap.put("wipeTopicCount", "1"); + return ServerResponseMocker.startServer(new byte[0], extMap); + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/offset/GetConsumerStatusCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/offset/GetConsumerStatusCommandTest.java new file mode 100644 index 0000000..0429aaf --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/offset/GetConsumerStatusCommandTest.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.tools.command.offset; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.protocol.body.GetConsumerStatusBody; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.apache.rocketmq.tools.command.server.NameServerMocker; +import org.apache.rocketmq.tools.command.server.ServerResponseMocker; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +public class GetConsumerStatusCommandTest { + + private ServerResponseMocker brokerMocker; + + private ServerResponseMocker nameServerMocker; + + @BeforeClass + public static void setUpEnv() { + System.setProperty("rocketmq.client.logRoot", System.getProperty("java.io.tmpdir")); + } + + @Before + public void before() { + brokerMocker = startOneBroker(); + nameServerMocker = NameServerMocker.startByDefaultConf(brokerMocker.listenPort()); + } + + @After + public void after() { + brokerMocker.shutdown(); + nameServerMocker.shutdown(); + } + + @Test + public void testExecute() throws SubCommandException { + GetConsumerStatusCommand cmd = new GetConsumerStatusCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-g default-group", "-t unit-test", "-i clientid", + String.format("-n localhost:%d", nameServerMocker.listenPort())}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + } + + private ServerResponseMocker startOneBroker() { + GetConsumerStatusBody getConsumerStatusBody = new GetConsumerStatusBody(); + // start broker + return ServerResponseMocker.startServer(getConsumerStatusBody.encode()); + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/offset/ResetOffsetByTimeCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/offset/ResetOffsetByTimeCommandTest.java new file mode 100644 index 0000000..d57edb8 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/offset/ResetOffsetByTimeCommandTest.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.tools.command.offset; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.remoting.protocol.body.ResetOffsetBody; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.apache.rocketmq.tools.command.server.NameServerMocker; +import org.apache.rocketmq.tools.command.server.ServerResponseMocker; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class ResetOffsetByTimeCommandTest { + + private ServerResponseMocker brokerMocker; + + private ServerResponseMocker nameServerMocker; + + @Before + public void before() { + brokerMocker = startOneBroker(); + nameServerMocker = NameServerMocker.startByDefaultConf(brokerMocker.listenPort()); + } + + @After + public void after() { + brokerMocker.shutdown(); + nameServerMocker.shutdown(); + } + + @Test + public void testExecute() throws SubCommandException { + ResetOffsetByTimeCommand cmd = new ResetOffsetByTimeCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] { + "-g default-group", "-t unit-test", "-s 1412131213231", "-f false", + String.format("-n localhost:%d", nameServerMocker.listenPort())}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + } + + private ServerResponseMocker startOneBroker() { + ResetOffsetBody resetOffsetBody = new ResetOffsetBody(); + // start broker + return ServerResponseMocker.startServer(resetOffsetBody.encode()); + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/offset/ResetOffsetByTimeOldCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/offset/ResetOffsetByTimeOldCommandTest.java new file mode 100644 index 0000000..fb5c543 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/offset/ResetOffsetByTimeOldCommandTest.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.tools.command.offset; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ResetOffsetByTimeOldCommandTest { + @Test + public void testExecute() { + ResetOffsetByTimeOldCommand cmd = new ResetOffsetByTimeOldCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-g default-group", "-t unit-test", "-s 1412131213231", "-f false"}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + assertThat(commandLine.getOptionValue('g').trim()).isEqualTo("default-group"); + assertThat(commandLine.getOptionValue('t').trim()).isEqualTo("unit-test"); + assertThat(commandLine.getOptionValue('s').trim()).isEqualTo("1412131213231"); + } +} \ No newline at end of file diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/offset/SkipAccumulationCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/offset/SkipAccumulationCommandTest.java new file mode 100644 index 0000000..36534f9 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/offset/SkipAccumulationCommandTest.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.tools.command.offset; + +import java.lang.reflect.Field; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.client.ClientConfig; +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.srvutil.ServerUtil; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExtImpl; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +import static org.mockito.Mockito.mock; + +public class SkipAccumulationCommandTest { + private static DefaultMQAdminExt defaultMQAdminExt; + private static DefaultMQAdminExtImpl defaultMQAdminExtImpl; + private static MQClientInstance mqClientInstance = MQClientManager.getInstance().getOrCreateMQClientInstance(new ClientConfig()); + private static MQClientAPIImpl mQClientAPIImpl; + + @BeforeClass + public static void init() throws Exception { + mQClientAPIImpl = mock(MQClientAPIImpl.class); + defaultMQAdminExt = new DefaultMQAdminExt(); + defaultMQAdminExtImpl = new DefaultMQAdminExtImpl(defaultMQAdminExt, 1000); + + Field field = DefaultMQAdminExtImpl.class.getDeclaredField("mqClientInstance"); + field.setAccessible(true); + field.set(defaultMQAdminExtImpl, mqClientInstance); + field = MQClientInstance.class.getDeclaredField("mQClientAPIImpl"); + field.setAccessible(true); + field.set(mqClientInstance, mQClientAPIImpl); + field = DefaultMQAdminExt.class.getDeclaredField("defaultMQAdminExtImpl"); + field.setAccessible(true); + field.set(defaultMQAdminExt, defaultMQAdminExtImpl); + } + + @AfterClass + public static void terminate() { + defaultMQAdminExt.shutdown(); + } + + @Ignore + @Test + public void testExecute() throws SubCommandException { + System.setProperty("rocketmq.namesrv.addr", "127.0.0.1:9876"); + SkipAccumulationSubCommand cmd = new SkipAccumulationSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-g group-test", "-t topic-test", "-f false"}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/producer/ProducerSubCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/producer/ProducerSubCommandTest.java new file mode 100644 index 0000000..7039c05 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/producer/ProducerSubCommandTest.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.tools.command.producer; + +import java.util.HashMap; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.admin.ConsumeStats; +import org.apache.rocketmq.remoting.protocol.admin.OffsetWrapper; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.tools.command.SubCommandException; +import org.apache.rocketmq.tools.command.server.NameServerMocker; +import org.apache.rocketmq.tools.command.server.ServerResponseMocker; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class ProducerSubCommandTest { + private ServerResponseMocker brokerMocker; + + private ServerResponseMocker nameServerMocker; + + @Before + public void before() { + brokerMocker = startOneBroker(); + nameServerMocker = NameServerMocker.startByDefaultConf(brokerMocker.listenPort()); + } + + @After + public void after() { + brokerMocker.shutdown(); + nameServerMocker.shutdown(); + } + + @Test + public void testExecute() throws SubCommandException { + ProducerSubCommand cmd = new ProducerSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[]{"-b 127.0.0.1:" + brokerMocker.listenPort()}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + cmd.execute(commandLine, options, null); + } + + private ServerResponseMocker startOneBroker() { + ConsumeStats consumeStats = new ConsumeStats(); + HashMap offsetTable = new HashMap<>(); + MessageQueue messageQueue = new MessageQueue(); + messageQueue.setBrokerName("mockBrokerName"); + messageQueue.setQueueId(1); + messageQueue.setBrokerName("mockTopicName"); + + OffsetWrapper offsetWrapper = new OffsetWrapper(); + offsetWrapper.setBrokerOffset(1); + offsetWrapper.setConsumerOffset(1); + offsetWrapper.setLastTimestamp(System.currentTimeMillis()); + + offsetTable.put(messageQueue, offsetWrapper); + consumeStats.setOffsetTable(offsetTable); + // start broker + return ServerResponseMocker.startServer(consumeStats.encode()); + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/server/NameServerMocker.java b/tools/src/test/java/org/apache/rocketmq/tools/command/server/NameServerMocker.java new file mode 100644 index 0000000..be46b6e --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/server/NameServerMocker.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.tools.command.server; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; + +/** + * tools class + */ +public class NameServerMocker { + + /** + * use the specified port to start the nameserver + * + * @param brokerPort broker port + * @return ServerResponseMocker + */ + public static ServerResponseMocker startByDefaultConf(int brokerPort) { + return startByDefaultConf(brokerPort, null); + } + + /** + * use the specified port to start the nameserver + * + * @param brokerPort broker port + * @param extMap extend config + * @return ServerResponseMocker + */ + public static ServerResponseMocker startByDefaultConf(int brokerPort, HashMap extMap) { + TopicRouteData topicRouteData = new TopicRouteData(); + List dataList = new ArrayList<>(); + HashMap brokerAddress = new HashMap<>(); + brokerAddress.put(1L, "127.0.0.1:" + brokerPort); + BrokerData brokerData = new BrokerData("mockCluster", "mockBrokerName", brokerAddress); + brokerData.setBrokerName("mockBrokerName"); + dataList.add(brokerData); + topicRouteData.setBrokerDatas(dataList); + // start name server + return ServerResponseMocker.startServer(topicRouteData.encode(), extMap); + } + +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/server/ServerResponseMocker.java b/tools/src/test/java/org/apache/rocketmq/tools/command/server/ServerResponseMocker.java new file mode 100644 index 0000000..94dca48 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/server/ServerResponseMocker.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.tools.command.server; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.timeout.IdleStateHandler; +import io.netty.util.concurrent.Future; +import java.net.InetSocketAddress; +import java.util.HashMap; +import java.util.concurrent.ExecutionException; +import org.apache.rocketmq.remoting.netty.NettyDecoder; +import org.apache.rocketmq.remoting.netty.NettyEncoder; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RemotingSysResponseCode; +import org.junit.After; +import org.junit.Before; + +/** + * mock server response for command + */ +public abstract class ServerResponseMocker { + + private int listenPort; + + private final NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup(); + + @Before + public void before() { + start(); + } + + @After + public void shutdown() { + if (eventLoopGroup.isShutdown()) { + return; + } + Future future = eventLoopGroup.shutdownGracefully(); + try { + future.get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + } + + public int listenPort() { + return listenPort; + } + + protected abstract byte[] getBody(); + + public void start() { + start(null); + } + + public void start(HashMap extMap) { + ServerBootstrap serverBootstrap = new ServerBootstrap(); + serverBootstrap.group(eventLoopGroup) + .channel(NioServerSocketChannel.class) + .option(ChannelOption.SO_BACKLOG, 1024) + .option(ChannelOption.SO_REUSEADDR, true) + .childOption(ChannelOption.SO_KEEPALIVE, false) + .childOption(ChannelOption.TCP_NODELAY, true) + .childOption(ChannelOption.SO_SNDBUF, 65535) + .childOption(ChannelOption.SO_RCVBUF, 65535) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ch.pipeline() + .addLast(eventLoopGroup, + new NettyEncoder(), + new NettyDecoder(), + new IdleStateHandler(0, 0, 120), + new ChannelDuplexHandler(), + new NettyServerHandler(extMap) + ); + } + }); + try { + ChannelFuture sync = serverBootstrap.bind(0).sync(); + InetSocketAddress addr = (InetSocketAddress) sync.channel().localAddress(); + this.listenPort = addr.getPort(); + } catch (InterruptedException e1) { + throw new RuntimeException("this.serverBootstrap.bind().sync() InterruptedException", e1); + } + } + + @ChannelHandler.Sharable + private class NettyServerHandler extends SimpleChannelInboundHandler { + private HashMap extMap; + + public NettyServerHandler(HashMap extMap) { + this.extMap = extMap; + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception { + String remark = "mock data"; + final RemotingCommand response = + RemotingCommand.createResponseCommand(RemotingSysResponseCode.SUCCESS, remark); + response.setOpaque(msg.getOpaque()); + response.setBody(getBody()); + + if (extMap != null && extMap.size() > 0) { + response.setExtFields(extMap); + } + ctx.writeAndFlush(response); + } + } + + public static ServerResponseMocker startServer(byte[] body) { + return startServer(body, null); + } + + + public static ServerResponseMocker startServer(byte[] body, HashMap extMap) { + ServerResponseMocker mocker = new ServerResponseMocker() { + @Override + protected byte[] getBody() { + return body; + } + }; + mocker.start(extMap); + // add jvm hook, close connection when jvm down + Runtime.getRuntime().addShutdownHook(new Thread(mocker::shutdown)); + return mocker; + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/topic/AllocateMQSubCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/topic/AllocateMQSubCommandTest.java new file mode 100644 index 0000000..2d9fb73 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/topic/AllocateMQSubCommandTest.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.tools.command.topic; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AllocateMQSubCommandTest { + @Test + public void testExecute() { + AllocateMQSubCommand cmd = new AllocateMQSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-t unit-test", "-i 127.0.0.1:10911"}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + assertThat(commandLine.getOptionValue('t').trim()).isEqualTo("unit-test"); + assertThat(commandLine.getOptionValue("i").trim()).isEqualTo("127.0.0.1:10911"); + } +} \ No newline at end of file diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/topic/DeleteTopicSubCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/topic/DeleteTopicSubCommandTest.java new file mode 100644 index 0000000..ac37204 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/topic/DeleteTopicSubCommandTest.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.tools.command.topic; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DeleteTopicSubCommandTest { + @Test + public void testExecute() { + DeleteTopicSubCommand cmd = new DeleteTopicSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-t unit-test", "-c default-cluster"}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + assertThat(commandLine.getOptionValue('t').trim()).isEqualTo("unit-test"); + assertThat(commandLine.getOptionValue("c").trim()).isEqualTo("default-cluster"); + } +} \ No newline at end of file diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/topic/TopicClusterSubCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/topic/TopicClusterSubCommandTest.java new file mode 100644 index 0000000..d4b54c5 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/topic/TopicClusterSubCommandTest.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.tools.command.topic; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TopicClusterSubCommandTest { + @Test + public void testExecute() { + TopicClusterSubCommand cmd = new TopicClusterSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-t unit-test"}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + assertThat(commandLine.getOptionValue('t').trim()).isEqualTo("unit-test"); + } +} \ No newline at end of file diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/topic/TopicRouteSubCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/topic/TopicRouteSubCommandTest.java new file mode 100644 index 0000000..26a6c79 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/topic/TopicRouteSubCommandTest.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.tools.command.topic; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TopicRouteSubCommandTest { + @Test + public void testExecute() { + TopicRouteSubCommand cmd = new TopicRouteSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-t unit-test"}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + assertThat(commandLine.getOptionValue('t').trim()).isEqualTo("unit-test"); + } +} \ No newline at end of file diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/topic/TopicStatusSubCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/topic/TopicStatusSubCommandTest.java new file mode 100644 index 0000000..d56e719 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/topic/TopicStatusSubCommandTest.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.tools.command.topic; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TopicStatusSubCommandTest { + @Test + public void testExecute() { + TopicStatusSubCommand cmd = new TopicStatusSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-t unit-test"}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + assertThat(commandLine.getOptionValue('t').trim()).isEqualTo("unit-test"); + } +} \ No newline at end of file diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/topic/UpdateOrderConfCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/topic/UpdateOrderConfCommandTest.java new file mode 100644 index 0000000..dfc3941 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/topic/UpdateOrderConfCommandTest.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.tools.command.topic; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class UpdateOrderConfCommandTest { + @Test + public void testExecute() { + UpdateOrderConfCommand cmd = new UpdateOrderConfCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-t unit-test", "-v default-broker:8", "-m post"}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + assertThat(commandLine.getOptionValue('t').trim()).isEqualTo("unit-test"); + assertThat(commandLine.getOptionValue('v').trim()).isEqualTo("default-broker:8"); + assertThat(commandLine.getOptionValue('m').trim()).isEqualTo("post"); + } +} \ No newline at end of file diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/topic/UpdateTopicListSubCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/topic/UpdateTopicListSubCommandTest.java new file mode 100644 index 0000000..71704a7 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/topic/UpdateTopicListSubCommandTest.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.tools.command.topic; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.junit.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class UpdateTopicListSubCommandTest { + + @Test + public void testArguments() { + UpdateTopicListSubCommand cmd = new UpdateTopicListSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-b 127.0.0.1:10911", "-f topics.json"}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + assertEquals("127.0.0.1:10911", commandLine.getOptionValue('b').trim()); + assertEquals("topics.json", commandLine.getOptionValue('f').trim()); + } +} \ No newline at end of file diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/topic/UpdateTopicPermSubCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/topic/UpdateTopicPermSubCommandTest.java new file mode 100644 index 0000000..ed642e8 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/topic/UpdateTopicPermSubCommandTest.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.tools.command.topic; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class UpdateTopicPermSubCommandTest { + @Test + public void testExecute() { + UpdateTopicPermSubCommand cmd = new UpdateTopicPermSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] {"-b 127.0.0.1:10911", "-c default-cluster", "-t unit-test", "-p 6"}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), subargs, + cmd.buildCommandlineOptions(options), new DefaultParser()); + assertThat(commandLine.getOptionValue('b').trim()).isEqualTo("127.0.0.1:10911"); + assertThat(commandLine.getOptionValue('c').trim()).isEqualTo("default-cluster"); + assertThat(commandLine.getOptionValue('t').trim()).isEqualTo("unit-test"); + assertThat(commandLine.getOptionValue('p').trim()).isEqualTo("6"); + + } +} \ No newline at end of file diff --git a/tools/src/test/java/org/apache/rocketmq/tools/command/topic/UpdateTopicSubCommandTest.java b/tools/src/test/java/org/apache/rocketmq/tools/command/topic/UpdateTopicSubCommandTest.java new file mode 100644 index 0000000..54c690e --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/command/topic/UpdateTopicSubCommandTest.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.tools.command.topic; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class UpdateTopicSubCommandTest { + @Test + public void testExecute() { + UpdateTopicSubCommand cmd = new UpdateTopicSubCommand(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + String[] subargs = new String[] { + "-b 127.0.0.1:10911", + "-t unit-test", + "-r 8", + "-w 8", + "-p 6", + "-o false", + "-u false", + "-s false"}; + final CommandLine commandLine = + ServerUtil.parseCmdLine("mqadmin " + cmd.commandName(), + subargs, cmd.buildCommandlineOptions(options), new DefaultParser()); + assertThat(commandLine.getOptionValue('b').trim()).isEqualTo("127.0.0.1:10911"); + assertThat(commandLine.getOptionValue('r').trim()).isEqualTo("8"); + assertThat(commandLine.getOptionValue('w').trim()).isEqualTo("8"); + assertThat(commandLine.getOptionValue('t').trim()).isEqualTo("unit-test"); + assertThat(commandLine.getOptionValue('p').trim()).isEqualTo("6"); + assertThat(commandLine.getOptionValue('o').trim()).isEqualTo("false"); + assertThat(commandLine.getOptionValue('u').trim()).isEqualTo("false"); + assertThat(commandLine.getOptionValue('s').trim()).isEqualTo("false"); + } +} \ No newline at end of file diff --git a/tools/src/test/java/org/apache/rocketmq/tools/monitor/DefaultMonitorListenerTest.java b/tools/src/test/java/org/apache/rocketmq/tools/monitor/DefaultMonitorListenerTest.java new file mode 100644 index 0000000..7200b46 --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/monitor/DefaultMonitorListenerTest.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.tools.monitor; + +import java.util.Properties; +import java.util.TreeMap; +import java.util.TreeSet; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.topic.OffsetMovedEvent; +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.Mockito.mock; + +public class DefaultMonitorListenerTest { + private DefaultMonitorListener defaultMonitorListener; + + @Before + public void init() { + defaultMonitorListener = mock(DefaultMonitorListener.class); + } + + @Test + public void testBeginRound() { + defaultMonitorListener.beginRound(); + } + + @Test + public void testReportUndoneMsgs() { + UndoneMsgs undoneMsgs = new UndoneMsgs(); + undoneMsgs.setConsumerGroup("default-group"); + undoneMsgs.setTopic("unit-test"); + undoneMsgs.setUndoneMsgsDelayTimeMills(30000); + undoneMsgs.setUndoneMsgsSingleMQ(1); + undoneMsgs.setUndoneMsgsTotal(100); + defaultMonitorListener.reportUndoneMsgs(undoneMsgs); + } + + @Test + public void testReportFailedMsgs() { + FailedMsgs failedMsgs = new FailedMsgs(); + failedMsgs.setTopic("unit-test"); + failedMsgs.setConsumerGroup("default-consumer"); + failedMsgs.setFailedMsgsTotalRecently(2); + defaultMonitorListener.reportFailedMsgs(failedMsgs); + } + + @Test + public void testReportDeleteMsgsEvent() { + DeleteMsgsEvent deleteMsgsEvent = new DeleteMsgsEvent(); + deleteMsgsEvent.setEventTimestamp(System.currentTimeMillis()); + deleteMsgsEvent.setOffsetMovedEvent(new OffsetMovedEvent()); + defaultMonitorListener.reportDeleteMsgsEvent(deleteMsgsEvent); + } + + @Test + public void testReportConsumerRunningInfo() { + TreeMap criTable = new TreeMap<>(); + ConsumerRunningInfo consumerRunningInfo = new ConsumerRunningInfo(); + consumerRunningInfo.setSubscriptionSet(new TreeSet<>()); + consumerRunningInfo.setStatusTable(new TreeMap<>()); + consumerRunningInfo.setSubscriptionSet(new TreeSet<>()); + consumerRunningInfo.setMqTable(new TreeMap<>()); + consumerRunningInfo.setProperties(new Properties()); + criTable.put("test", consumerRunningInfo); + defaultMonitorListener.reportConsumerRunningInfo(criTable); + } +} diff --git a/tools/src/test/java/org/apache/rocketmq/tools/monitor/MonitorServiceTest.java b/tools/src/test/java/org/apache/rocketmq/tools/monitor/MonitorServiceTest.java new file mode 100644 index 0000000..e459f5d --- /dev/null +++ b/tools/src/test/java/org/apache/rocketmq/tools/monitor/MonitorServiceTest.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.tools.monitor; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.exception.MQBrokerException; +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.MQVersion; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.apache.rocketmq.remoting.protocol.admin.ConsumeStats; +import org.apache.rocketmq.remoting.protocol.admin.OffsetWrapper; +import org.apache.rocketmq.remoting.protocol.body.Connection; +import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.body.TopicList; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +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.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType.CONSUME_ACTIVELY; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MonitorServiceTest { + private static DefaultMQAdminExt defaultMQAdminExt; + private static DefaultMQAdminExtImpl defaultMQAdminExtImpl; + private static MQClientInstance mqClientInstance = MQClientManager.getInstance().getOrCreateMQClientInstance(new ClientConfig()); + private static MQClientAPIImpl mQClientAPIImpl; + private static MonitorConfig monitorConfig; + private static MonitorListener monitorListener; + private static DefaultMQPullConsumer defaultMQPullConsumer; + private static DefaultMQPushConsumer defaultMQPushConsumer; + private static MonitorService monitorService; + + @BeforeClass + public static void init() throws NoSuchFieldException, IllegalAccessException, RemotingException, MQClientException, InterruptedException, MQBrokerException { + monitorConfig = new MonitorConfig(); + monitorListener = new DefaultMonitorListener(); + defaultMQPullConsumer = mock(DefaultMQPullConsumer.class); + defaultMQPushConsumer = mock(DefaultMQPushConsumer.class); + mQClientAPIImpl = mock(MQClientAPIImpl.class); + defaultMQAdminExt = new DefaultMQAdminExt(); + defaultMQAdminExtImpl = new DefaultMQAdminExtImpl(defaultMQAdminExt, 1000); + monitorService = new MonitorService(monitorConfig, monitorListener, null); + + Field field = DefaultMQAdminExtImpl.class.getDeclaredField("mqClientInstance"); + field.setAccessible(true); + field.set(defaultMQAdminExtImpl, mqClientInstance); + field = MQClientInstance.class.getDeclaredField("mQClientAPIImpl"); + field.setAccessible(true); + field.set(mqClientInstance, mQClientAPIImpl); + field = DefaultMQAdminExt.class.getDeclaredField("defaultMQAdminExtImpl"); + field.setAccessible(true); + field.set(defaultMQAdminExt, defaultMQAdminExtImpl); + + field = MonitorService.class.getDeclaredField("defaultMQAdminExt"); + field.setAccessible(true); + field.set(monitorService, defaultMQAdminExt); + field = MonitorService.class.getDeclaredField("defaultMQPullConsumer"); + field.setAccessible(true); + field.set(monitorService, defaultMQPullConsumer); + field = MonitorService.class.getDeclaredField("defaultMQPushConsumer"); + field.setAccessible(true); + field.set(monitorService, defaultMQPushConsumer); + + TopicList topicList = new TopicList(); + Set topicSet = new HashSet<>(); + topicSet.add("topic_one"); + topicSet.add("topic_two"); + topicList.setTopicList(topicSet); + when(mQClientAPIImpl.getTopicListFromNameServer(anyLong())).thenReturn(topicList); + + TopicRouteData topicRouteData = new TopicRouteData(); + List brokerDatas = new ArrayList<>(); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(1234L, "127.0.0.1:10911"); + BrokerData brokerData = new BrokerData(); + brokerData.setCluster("default-cluster"); + brokerData.setBrokerName("default-broker"); + brokerData.setBrokerAddrs(brokerAddrs); + brokerDatas.add(brokerData); + topicRouteData.setBrokerDatas(brokerDatas); + topicRouteData.setQueueDatas(new ArrayList<>()); + topicRouteData.setFilterServerTable(new HashMap<>()); + when(mQClientAPIImpl.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(topicRouteData); + + ConsumeStats consumeStats = new ConsumeStats(); + consumeStats.setConsumeTps(1234); + MessageQueue messageQueue = new MessageQueue(); + OffsetWrapper offsetWrapper = new OffsetWrapper(); + HashMap stats = new HashMap<>(); + stats.put(messageQueue, offsetWrapper); + consumeStats.setOffsetTable(stats); + when(mQClientAPIImpl.getConsumeStats(anyString(), anyString(), anyString(), anyLong())).thenReturn(consumeStats); + + ConsumerConnection consumerConnection = new ConsumerConnection(); + consumerConnection.setConsumeType(ConsumeType.CONSUME_PASSIVELY); + consumerConnection.setMessageModel(MessageModel.CLUSTERING); + HashSet connections = new HashSet<>(); + Connection connection = new Connection(); + connection.setClientId("client_id"); + connection.setClientAddr("127.0.0.1:109111"); + connection.setLanguage(LanguageCode.JAVA); + connection.setVersion(MQVersion.Version.V4_0_0_SNAPSHOT.ordinal()); + connections.add(connection); + consumerConnection.setConnectionSet(connections); + consumerConnection.setSubscriptionTable(new ConcurrentHashMap<>()); + consumerConnection.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + when(mQClientAPIImpl.getConsumerConnectionList(anyString(), anyString(), anyLong())).thenReturn(consumerConnection); + + ConsumerRunningInfo consumerRunningInfo = new ConsumerRunningInfo(); + consumerRunningInfo.setJstack("test"); + consumerRunningInfo.setMqTable(new TreeMap<>()); + consumerRunningInfo.setStatusTable(new TreeMap<>()); + consumerRunningInfo.setSubscriptionSet(new TreeSet<>()); + Properties properties = new Properties(); + properties.put(ConsumerRunningInfo.PROP_CONSUME_TYPE, CONSUME_ACTIVELY); + properties.put(ConsumerRunningInfo.PROP_CONSUMER_START_TIMESTAMP, System.currentTimeMillis()); + consumerRunningInfo.setProperties(properties); + when(mQClientAPIImpl.getConsumerRunningInfo(anyString(), anyString(), anyString(), anyBoolean(), anyLong())).thenReturn(consumerRunningInfo); + } + + @AfterClass + public static void terminate() { + } + + @Test + public void testDoMonitorWork() throws RemotingException, MQClientException, InterruptedException { + monitorService.doMonitorWork(); + } + + @Test + public void testReportConsumerRunningInfo() throws InterruptedException, RemotingException, MQClientException, MQBrokerException { + monitorService.reportConsumerRunningInfo("test_group"); + } +} diff --git a/tools/src/test/resources/rmq.logback-test.xml b/tools/src/test/resources/rmq.logback-test.xml new file mode 100644 index 0000000..8695d52 --- /dev/null +++ b/tools/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